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:
Natsumi
2025-01-11 13:09:44 +13:00
committed by GitHub
parent a39eb9d5ed
commit 938fff63d0
223 changed files with 15841 additions and 9562 deletions

View File

@@ -1,727 +0,0 @@
// 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 AppApi
{
public static readonly AppApi Instance;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly MD5 _hasher = MD5.Create();
static AppApi()
{
Instance = new AppApi();
ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged;
ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged;
}
public void Init()
{
// Create Instance before Cef tries to bind it
}
/// <summary>
/// Computes the MD5 hash of the file represented by the specified base64-encoded string.
/// </summary>
/// <param name="Blob">The base64-encoded string representing the file.</param>
/// <returns>The MD5 hash of the file as a base64-encoded string.</returns>
public string MD5File(string Blob)
{
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
using (var md5 = MD5.Create())
{
var md5Hash = md5.ComputeHash(fileData);
return Convert.ToBase64String(md5Hash);
}
}
public string ResizeImageToFitLimits(string base64data)
{
return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false));
}
public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000)
{
using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream);
// for APNG, check if image is png format and less than maxSize
if ((!matchingDimensions || image.Width == image.Height) &&
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) &&
imageData.Length < maxSize &&
image.Width <= maxWidth &&
image.Height <= maxHeight)
{
return imageData;
}
if (image.Width > maxWidth)
{
var sizingFactor = image.Width / (double)maxWidth;
var newHeight = (int)Math.Round(image.Height / sizingFactor);
image = new Bitmap(image, maxWidth, newHeight);
}
if (image.Height > maxHeight)
{
var sizingFactor = image.Height / (double)maxHeight;
var newWidth = (int)Math.Round(image.Width / sizingFactor);
image = new Bitmap(image, newWidth, maxHeight);
}
if (matchingDimensions && image.Width != image.Height)
{
var newSize = Math.Max(image.Width, image.Height);
var newImage = new Bitmap(newSize, newSize);
using var graphics = Graphics.FromImage(newImage);
graphics.Clear(Color.Transparent);
graphics.DrawImage(image, new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height));
image.Dispose();
image = newImage;
}
SaveToFileToUpload();
for (int i = 0; i < 250 && imageData.Length > maxSize; i++)
{
SaveToFileToUpload();
if (imageData.Length < maxSize)
break;
int newWidth;
int newHeight;
if (image.Width > image.Height)
{
newWidth = image.Width - 25;
newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth));
}
else
{
newHeight = image.Height - 25;
newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight));
}
image = new Bitmap(image, newWidth, newHeight);
}
if (imageData.Length > maxSize)
{
throw new Exception("Failed to get image into target filesize.");
}
return imageData;
void SaveToFileToUpload()
{
using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
imageData = imageSaveMemoryStream.ToArray();
}
}
public byte[] ResizePrintImage(byte[] imageData)
{
const int desiredWidth = 1920;
const int desiredHeight = 1080;
using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream);
// increase size to 1920x1080
if (image.Width < desiredWidth || image.Height < desiredHeight)
{
var newHeight = image.Height;
var newWidth = image.Width;
if (image.Width < desiredWidth)
{
var testHeight = (int)Math.Round(image.Height / (image.Width / (double)desiredWidth));
if (testHeight <= desiredHeight)
{
newWidth = desiredWidth;
newHeight = testHeight;
}
}
if (image.Height < desiredHeight)
{
var testWidth = (int)Math.Round(image.Width / (image.Height / (double)desiredHeight));
if (testWidth <= desiredWidth)
{
newHeight = desiredHeight;
newWidth = testWidth;
}
}
var resizedImage = new Bitmap(desiredWidth, desiredHeight);
using var graphics1 = Graphics.FromImage(resizedImage);
graphics1.Clear(Color.White);
var x = (desiredWidth - newWidth) / 2;
var y = (desiredHeight - newHeight) / 2;
graphics1.DrawImage(image, new Rectangle(x, y, newWidth, newHeight));
image.Dispose();
image = resizedImage;
}
// limit size to 1920x1080
if (image.Width > desiredWidth)
{
var sizingFactor = image.Width / (double)desiredWidth;
var newHeight = (int)Math.Round(image.Height / sizingFactor);
image = new Bitmap(image, desiredWidth, newHeight);
}
if (image.Height > desiredHeight)
{
var sizingFactor = image.Height / (double)desiredHeight;
var newWidth = (int)Math.Round(image.Width / sizingFactor);
image = new Bitmap(image, newWidth, desiredHeight);
}
// add white border
// wtf are these magic numbers
const int xOffset = 64; // 2048 / 32
const int yOffset = 69; // 1440 / 20.869
var newImage = new Bitmap(2048, 1440);
using var graphics = Graphics.FromImage(newImage);
graphics.Clear(Color.White);
// graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
var newX = (2048 - image.Width) / 2;
var newY = yOffset;
graphics.DrawImage(image, new Rectangle(newX, newY, image.Width, image.Height));
image.Dispose();
image = newImage;
using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
return imageSaveMemoryStream.ToArray();
}
public async Task CropAllPrints(string ugcFolderPath)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints");
var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories);
foreach (var file in files)
{
await CropPrintImage(file);
}
}
public async Task<bool> CropPrintImage(string path)
{
var tempPath = path + ".temp";
var bytes = await File.ReadAllBytesAsync(path);
var ms = new MemoryStream(bytes);
Bitmap print = new Bitmap(ms);
// validation step to ensure image is actually a print
if (print.Width != 2048 || print.Height != 1440)
{
return false;
}
var point = new Point(64, 69);
var size = new Size(1920, 1080);
var rectangle = new Rectangle(point, size);
Bitmap cropped = print.Clone(rectangle, print.PixelFormat);
cropped.Save(tempPath);
if (ScreenshotHelper.HasTXt(path))
{
var success = ScreenshotHelper.CopyTXt(path, tempPath);
if (!success)
{
File.Delete(tempPath);
return false;
}
}
File.Move(tempPath, path, true);
return true;
}
/// <summary>
/// Computes the signature of the file represented by the specified base64-encoded string using the librsync library.
/// </summary>
/// <param name="Blob">The base64-encoded string representing the file.</param>
/// <returns>The signature of the file as a base64-encoded string.</returns>
public string SignFile(string Blob)
{
var fileData = Convert.FromBase64String(Blob);
using (var sig = Librsync.ComputeSignature(new MemoryStream(fileData)))
using (var memoryStream = new MemoryStream())
{
sig.CopyTo(memoryStream);
var sigBytes = memoryStream.ToArray();
return Convert.ToBase64String(sigBytes);
}
}
/// <summary>
/// Returns the length of the file represented by the specified base64-encoded string.
/// </summary>
/// <param name="Blob">The base64-encoded string representing the file.</param>
/// <returns>The length of the file in bytes.</returns>
public string FileLength(string Blob)
{
var fileData = Convert.FromBase64String(Blob);
return fileData.Length.ToString();
}
/// <summary>
/// Shows the developer tools for the main browser window.
/// </summary>
public void ShowDevTools()
{
MainForm.Instance.Browser.ShowDevTools();
}
/// <summary>
/// Deletes all cookies from the global cef cookie manager.
/// </summary>
public void DeleteAllCookies()
{
Cef.GetGlobalCookieManager().DeleteCookies();
}
/// <summary>
/// Opens the specified URL in the default browser.
/// </summary>
/// <param name="url">The URL to open.</param>
public void OpenLink(string url)
{
if (url.StartsWith("http://") ||
url.StartsWith("https://"))
{
Process.Start(new ProcessStartInfo(url)
{
UseShellExecute = true
});
}
}
// broken since adding ExecuteVrFeedFunction(
// public void ShowVRForm()
// {
// try
// {
// MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
// {
// if (VRForm.Instance == null)
// {
// new VRForm().Show();
// }
// }));
// }
// catch
// {
// }
// }
public void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
{
Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand);
}
public void RefreshVR()
{
Program.VRCXVRInstance.Restart();
}
public void RestartVR()
{
Program.VRCXVRInstance.Restart();
}
public void SetZoom(double zoomLevel)
{
MainForm.Instance.Browser.SetZoomLevel(zoomLevel);
}
public async Task<double> GetZoom()
{
return await MainForm.Instance.Browser.GetZoomLevelAsync();
}
/// <summary>
/// Retrieves an image from the VRChat API and caches it for future use. The function will return the cached image if it already exists.
/// </summary>
/// <param name="url">The URL of the image to retrieve.</param>
/// <param name="fileId">The ID of the file associated with the image.</param>
/// <param name="version">The version of the file associated with the image.</param>
/// <returns>A string representing the file location of the cached image.</returns>
public async Task<string> GetImage(string url, string fileId, string version)
{
return await ImageCache.GetImage(url, fileId, version);
}
/// <summary>
/// Displays a desktop notification with the specified bold text, optional text, and optional image.
/// </summary>
/// <param name="BoldText">The bold text to display in the notification.</param>
/// <param name="Text">The optional text to display in the notification.</param>
/// <param name="Image">The optional image to display in the notification.</param>
public 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");
}
}
/// <summary>
/// Restarts the VRCX application for an update by launching a new process with the upgrade argument and exiting the current process.
/// </summary>
public 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);
}
/// <summary>
/// Checks if the VRCX update executable exists in the AppData directory.
/// </summary>
/// <returns>True if the update executable exists, false otherwise.</returns>
public bool CheckForUpdateExe()
{
if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe")))
return true;
return false;
}
/// <summary>
/// Sends an IPC packet to announce the start of VRCX.
/// </summary>
public void IPCAnnounceStart()
{
IPCServer.Send(new IPCPacket
{
Type = "VRCXLaunch",
MsgType = "VRCXLaunch"
});
}
/// <summary>
/// Sends an IPC packet with a specified message type and data.
/// </summary>
/// <param name="type">The message type to send.</param>
/// <param name="data">The data to send.</param>
public void SendIpc(string type, string data)
{
IPCServer.Send(new IPCPacket
{
Type = "VrcxMessage",
MsgType = type,
Data = data
});
}
public 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 void ExecuteVrFeedFunction(string function, string json)
{
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);
}
public void ExecuteVrOverlayFunction(string function, string json)
{
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
}
/// <summary>
/// Gets the launch command from the startup arguments and clears the launch command.
/// </summary>
/// <returns>The launch command.</returns>
public string GetLaunchCommand()
{
var command = StartupArgs.LaunchArguments.LaunchCommand;
StartupArgs.LaunchArguments.LaunchCommand = string.Empty;
return command;
}
/// <summary>
/// Focuses the main window of the VRCX application.
/// </summary>
public void FocusWindow()
{
MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); }));
}
/// <summary>
/// Returns the file path of the custom user CSS file, if it exists.
/// </summary>
/// <returns>The file path of the custom user CSS file, or an empty string if it doesn't exist.</returns>
public string CustomCssPath()
{
var output = string.Empty;
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.css");
if (File.Exists(filePath))
output = filePath;
return output;
}
/// <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 CustomScriptPath()
{
var output = string.Empty;
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.js");
if (File.Exists(filePath))
output = filePath;
return output;
}
public string CurrentCulture()
{
return CultureInfo.CurrentCulture.ToString();
}
public string CurrentLanguage()
{
return CultureInfo.InstalledUICulture.Name;
}
public string GetVersion()
{
return Program.Version;
}
/// <summary>
/// Returns whether or not the VRChat client was last closed gracefully. According to the log file, anyway.
/// </summary>
/// <returns>True if the VRChat client was last closed gracefully, false otherwise.</returns>
public bool VrcClosedGracefully()
{
return LogWatcher.Instance.VrcClosedGracefully;
}
public void ChangeTheme(int value)
{
WinformThemer.SetGlobalTheme(value);
}
public void DoFunny()
{
WinformThemer.DoFunny();
}
/// <summary>
/// Returns a color value derived from the given user ID.
/// This is, essentially, and is used for, random colors.
/// </summary>
/// <param name="userId">The user ID to derive the color value from.</param>
/// <returns>A color value derived from the given user ID.</returns>
public int GetColourFromUserID(string userId)
{
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
return (hash[3] << 8) | hash[4];
}
/// <summary>
/// Returns a dictionary of color values derived from the given list of user IDs.
/// </summary>
/// <param name="userIds">The list of user IDs to derive the color values from.</param>
/// <returns>A dictionary of color values derived from the given list of user IDs.</returns>
public Dictionary<string, int> GetColourBulk(List<object> userIds)
{
var output = new Dictionary<string, int>();
foreach (string userId in userIds)
{
output.Add(userId, GetColourFromUserID(userId));
}
return output;
}
/// <summary>
/// Retrieves the current text from the clipboard.
/// </summary>
/// <returns>The current text from the clipboard.</returns>
public string GetClipboard()
{
var clipboard = string.Empty;
var thread = new Thread(() => clipboard = Clipboard.GetText());
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
thread.Join();
return clipboard;
}
/// <summary>
/// Sets whether or not the application should start up automatically with Windows.
/// </summary>
/// <param name="enabled">True to enable automatic startup, false to disable it.</param>
public void SetStartup(bool enabled)
{
try
{
using (var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true))
{
if (enabled)
{
var path = System.Windows.Forms.Application.ExecutablePath;
key.SetValue("VRCX", $"\"{path}\" --startup");
}
else
{
key.DeleteValue("VRCX", false);
}
}
}
catch
{
}
}
// what the fuck even is this
// refactor when
// #AppApiLivesDontMatter
public void SetAppLauncherSettings(bool enabled, bool killOnExit)
{
AutoAppLaunchManager.Instance.Enabled = enabled;
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
}
/// <summary>
/// Copies an image file to the clipboard if it exists and is of a supported image file type.
/// </summary>
/// <param name="path">The path to the image file to copy to the clipboard.</param>
public void CopyImageToClipboard(string path)
{
// check if the file exists and is any image file type
if (File.Exists(path) && (path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") || path.EndsWith(".gif") || path.EndsWith(".bmp") || path.EndsWith(".webp")))
{
MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
{
var image = System.Drawing.Image.FromFile(path);
// Clipboard.SetImage(image);
var data = new DataObject();
data.SetData(DataFormats.Bitmap, image);
data.SetFileDropList(new StringCollection { path });
Clipboard.SetDataObject(data, true);
}));
}
}
/// <summary>
/// Flashes the window of the main form.
/// </summary>
public void FlashWindow()
{
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); }));
}
/// <summary>
/// Sets the user agent string for the browser.
/// </summary>
public void SetUserAgent()
{
using (var client = MainForm.Instance.Browser.GetDevToolsClient())
{
_ = client.Network.SetUserAgentOverrideAsync(Program.Version);
}
}
public string GetFileBase64(string path)
{
if (File.Exists(path))
{
return Convert.ToBase64String(File.ReadAllBytes(path));
}
return null;
}
public async Task<string> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return null;
var success = await ImageCache.SaveImageToFile(url, filePath);
return success ? filePath : null;
}
public async Task<string> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return null;
var success = await ImageCache.SaveImageToFile(url, filePath);
return success ? filePath : null;
}
public bool IsRunningUnderWine()
{
return Wine.GetIfWine();
}
}
}

View 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();
}
}
}

View File

@@ -12,14 +12,14 @@ using System.Threading.Tasks;
namespace VRCX
{
public partial class AppApi
public partial class AppApiCef
{
/// <summary>
/// Gets the VRChat application data location by reading the config file and checking the cache directory.
/// If the cache directory is not found in the config file, it returns the default cache path.
/// </summary>
/// <returns>The VRChat application data location.</returns>
public string GetVRChatAppDataLocation()
public override string GetVRChatAppDataLocation()
{
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
}
public override string GetVRChatCacheLocation()
{
var json = ReadConfigFile();
if (!string.IsNullOrEmpty(json))
@@ -35,10 +35,10 @@ namespace VRCX
}
}
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
}
public string GetVRChatPhotosLocation()
public override string GetVRChatPhotosLocation()
{
var json = ReadConfigFile();
if (!string.IsNullOrEmpty(json))
@@ -56,13 +56,8 @@ namespace VRCX
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat");
}
/// <summary>
/// Gets the folder the user has selected for User-Generated content such as prints / stickers from the JS side.
/// If there is no override on the folder, it returns the default VRChat Photos path.
/// </summary>
/// <returns>The UGC Photo Location.</returns>
public string GetUGCPhotoLocation(string path = "")
public override string GetUGCPhotoLocation(string path = "")
{
if (string.IsNullOrEmpty(path))
{
@@ -102,14 +97,15 @@ namespace VRCX
}
}
}
catch
catch (Exception e)
{
logger.Error($"Failed to get Steam userdata path from registry: {e}");
}
return steamUserdataPath;
}
public string GetVRChatScreenshotsLocation()
public override string GetVRChatScreenshotsLocation()
{
// program files steam userdata screenshots
var steamUserdataPath = GetSteamUserdataPathFromRegistry();
@@ -136,16 +132,7 @@ namespace VRCX
return screenshotPath;
}
/// <summary>
/// Gets the VRChat cache location by combining the VRChat application data location with the cache directory name.
/// </summary>
/// <returns>The VRChat cache location.</returns>
public string GetVRChatCacheLocation()
{
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
}
public bool OpenVrcxAppDataFolder()
public override bool OpenVrcxAppDataFolder()
{
var path = Program.AppDataDirectory;
if (!Directory.Exists(path))
@@ -155,7 +142,7 @@ namespace VRCX
return true;
}
public bool OpenVrcAppDataFolder()
public override bool OpenVrcAppDataFolder()
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
if (!Directory.Exists(path))
@@ -165,7 +152,7 @@ namespace VRCX
return true;
}
public bool OpenVrcPhotosFolder()
public override bool OpenVrcPhotosFolder()
{
var path = GetVRChatPhotosLocation();
if (!Directory.Exists(path))
@@ -175,7 +162,7 @@ namespace VRCX
return true;
}
public bool OpenUGCPhotosFolder(string ugcPath = "")
public override bool OpenUGCPhotosFolder(string ugcPath = "")
{
var path = GetUGCPhotoLocation(ugcPath);
if (!Directory.Exists(path))
@@ -185,7 +172,7 @@ namespace VRCX
return true;
}
public bool OpenVrcScreenshotsFolder()
public override bool OpenVrcScreenshotsFolder()
{
var path = GetVRChatScreenshotsLocation();
if (!Directory.Exists(path))
@@ -195,7 +182,7 @@ namespace VRCX
return true;
}
public bool OpenCrashVrcCrashDumps()
public override bool OpenCrashVrcCrashDumps()
{
var path = Path.Combine(Path.GetTempPath(), "VRChat", "VRChat", "Crashes");
if (!Directory.Exists(path))
@@ -204,11 +191,8 @@ namespace VRCX
OpenFolderAndSelectItem(path, true);
return true;
}
/// <summary>
/// Opens the folder containing user-defined shortcuts, if it exists.
/// </summary>
public void OpenShortcutFolder()
public override void OpenShortcutFolder()
{
var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
if (!Directory.Exists(path))
@@ -216,13 +200,8 @@ namespace VRCX
OpenFolderAndSelectItem(path, true);
}
/// <summary>
/// Opens the folder containing the specified file or folder path and selects the item in the folder.
/// </summary>
/// <param name="path">The path to the file or folder to select in the folder.</param>
/// <param name="isFolder">Whether the specified path is a folder or not. Defaults to false.</param>
public void OpenFolderAndSelectItem(string path, bool isFolder = false)
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.
@@ -272,7 +251,7 @@ namespace VRCX
}
}
public void OpenFolderAndSelectItemFallback(string path)
private void OpenFolderAndSelectItemFallback(string path)
{
if (!File.Exists(path) && !Directory.Exists(path))
return;
@@ -288,30 +267,24 @@ namespace VRCX
}
}
/// <summary>
/// Opens a folder dialog to select a folder and pass it back to the JS side.
/// </summary>
/// <param name="defaultPath">The default path for the folder picker.</param>
public async Task<string> OpenFolderSelectorDialog(string defaultPath = "")
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();
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);
}
var dialogResult = openFolderDialog.ShowDialog(MainForm.nativeWindow);
if (dialogResult == DialogResult.OK)
{
tcs.SetResult(openFolderDialog.SelectedPath);
}
else
{
tcs.SetResult(defaultPath);
}
}
catch (Exception ex)
@@ -325,12 +298,8 @@ namespace VRCX
return await tcs.Task;
}
/// <summary>
/// Opens a folder dialog to select a file and pass the path back to the JS side.
/// </summary>
/// <param name="defaultPath">The default path for the file picker.</param>
public async Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "", string defaultFilter = "All files (*.*)|*.*")
public override async Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "", string defaultFilter = "All files (*.*)|*.*")
{
var tcs = new TaskCompletionSource<string>();
var staThread = new Thread(() =>
@@ -369,21 +338,5 @@ namespace VRCX
return await tcs.Task;
}
private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
Regex.Escape(new string(Path.GetInvalidPathChars()))));
private static readonly Regex _fileRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
Regex.Escape(new string(Path.GetInvalidFileNameChars()))));
public static string MakeValidFileName(string name)
{
name = name.Replace("/", "");
name = name.Replace("\\", "");
name = _folderRegex.Replace(name, "");
name = _fileRegex.Replace(name, "");
return name;
}
}
}

View 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;
}
}
}

View File

@@ -11,10 +11,10 @@ using Microsoft.Win32;
namespace VRCX
{
public partial class AppApi
public partial class AppApiCef
{
[DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern uint RegSetValueEx(
private static extern uint RegSetValueEx(
UIntPtr hKey,
[MarshalAs(UnmanagedType.LPStr)] string lpValueName,
int Reserved,
@@ -23,7 +23,7 @@ namespace VRCX
int cbData);
[DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
public static extern int RegOpenKeyEx(
private static extern int RegOpenKeyEx(
UIntPtr hKey,
string subKey,
int ulOptions,
@@ -31,9 +31,9 @@ namespace VRCX
out UIntPtr hkResult);
[DllImport("advapi32.dll")]
public static extern int RegCloseKey(UIntPtr hKey);
private static extern int RegCloseKey(UIntPtr hKey);
public string AddHashToKeyName(string key)
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
@@ -48,35 +48,39 @@ namespace VRCX
/// </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 object GetVRChatRegistryKey(string key)
public override object GetVRChatRegistryKey(string key)
{
var keyName = AddHashToKeyName(key);
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
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)
{
var data = regKey?.GetValue(keyName);
if (data == null)
return null;
case RegistryValueKind.Binary:
return Encoding.ASCII.GetString((byte[])data);
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;
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;
}
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>
@@ -84,33 +88,31 @@ namespace VRCX
/// <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 bool SetVRChatRegistryKey(string key, object value, int typeInt)
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))
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat", true);
if (regKey == null)
return false;
object setValue = null;
switch (type)
{
if (regKey == null)
return false;
case RegistryValueKind.Binary:
setValue = Encoding.ASCII.GetBytes(value.ToString());
break;
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);
case RegistryValueKind.DWord:
setValue = value;
break;
}
if (setValue == null)
return false;
regKey.SetValue(keyName, setValue, type);
return true;
}
@@ -119,7 +121,7 @@ namespace VRCX
/// </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 void SetVRChatRegistryKey(string key, byte[] value)
public override void SetVRChatRegistryKey(string key, byte[] value)
{
var keyName = AddHashToKeyName(key);
var hKey = (UIntPtr)0x80000001; // HKEY_LOCAL_MACHINE
@@ -136,78 +138,77 @@ namespace VRCX
RegCloseKey(hKey);
}
public Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
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"))
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)
{
if (regKey == null)
throw new Exception("Nothing to backup.");
var data = regKey.GetValue(key);
var index = key.LastIndexOf("_h", StringComparison.Ordinal);
if (index <= 0)
continue;
var keys = regKey.GetValueNames();
var keyName = key.Substring(0, index);
if (data == null)
continue;
Span<long> spanLong = stackalloc long[1];
Span<double> doubleSpan = MemoryMarshal.Cast<long, double>(spanLong);
foreach (var key in keys)
var type = regKey.GetValueKind(key);
switch (type)
{
var data = regKey.GetValue(key);
var index = key.LastIndexOf("_h", StringComparison.Ordinal);
if (index <= 0)
continue;
case RegistryValueKind.Binary:
var binDict = new Dictionary<string, object>
{
{ "data", Encoding.ASCII.GetString((byte[])data) },
{ "type", type }
};
output.Add(keyName, binDict);
break;
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>
case RegistryValueKind.DWord:
if (data.GetType() != typeof(long))
{
var dwordDict = new Dictionary<string, object>
{
{ "data", Encoding.ASCII.GetString((byte[])data) },
{ "data", data },
{ "type", type }
};
output.Add(keyName, binDict);
output.Add(keyName, dwordDict);
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;
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;
}
default:
Debug.WriteLine($"Unknown registry value kind: {type}");
break;
}
}
return output;
}
public void SetVRChatRegistry(string json)
public override void SetVRChatRegistry(string json)
{
CreateVRChatRegistryFolder();
Span<double> spanDouble = stackalloc double[1];
var dict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json);
var dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json);
foreach (var item in dict)
{
var data = (JsonElement)item.Value["data"];
@@ -241,45 +242,36 @@ namespace VRCX
}
}
public bool HasVRChatRegistryFolder()
public override bool HasVRChatRegistryFolder()
{
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
{
return regKey != null;
}
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
return regKey != null;
}
public void CreateVRChatRegistryFolder()
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.");
}
using var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\VRChat\VRChat");
if (key == null)
throw new Exception("Error creating registry key.");
}
public void DeleteVRChatRegistryFolder()
public override void DeleteVRChatRegistryFolder()
{
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
{
if (regKey == null)
return;
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
if (regKey == null)
return;
Registry.CurrentUser.DeleteSubKeyTree(@"SOFTWARE\VRChat\VRChat");
}
Registry.CurrentUser.DeleteSubKeyTree(@"SOFTWARE\VRChat\VRChat");
}
public string ReadVrcRegJsonFile(string filepath)
public override string ReadVrcRegJsonFile(string filepath)
{
if (!File.Exists(filepath))
{
return "";
}
return string.Empty;
var json = File.ReadAllText(filepath);
return json;
}

View 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;
}
}
}

View File

@@ -0,0 +1,148 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using librsync.net;
using NLog;
namespace VRCX
{
public partial class AppApi
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
private static readonly MD5 _hasher = MD5.Create();
public void Init()
{
}
public string MD5File(string blob)
{
var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length);
using var md5 = MD5.Create();
var md5Hash = md5.ComputeHash(fileData);
return Convert.ToBase64String(md5Hash);
}
public int GetColourFromUserID(string userId)
{
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
return (hash[3] << 8) | hash[4];
}
public string SignFile(string blob)
{
var fileData = Convert.FromBase64String(blob);
using var sig = Librsync.ComputeSignature(new MemoryStream(fileData));
using var memoryStream = new MemoryStream();
sig.CopyTo(memoryStream);
var sigBytes = memoryStream.ToArray();
return Convert.ToBase64String(sigBytes);
}
public string FileLength(string blob)
{
var fileData = Convert.FromBase64String(blob);
return fileData.Length.ToString();
}
public void OpenLink(string url)
{
if (url.StartsWith("http://") ||
url.StartsWith("https://"))
{
Process.Start(new ProcessStartInfo(url)
{
UseShellExecute = true
});
}
}
public void IPCAnnounceStart()
{
IPCServer.Send(new IPCPacket
{
Type = "VRCXLaunch",
MsgType = "VRCXLaunch"
});
}
public void SendIpc(string type, string data)
{
IPCServer.Send(new IPCPacket
{
Type = "VrcxMessage",
MsgType = type,
Data = data
});
}
public string CustomCssPath()
{
var output = string.Empty;
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.css");
if (File.Exists(filePath))
output = filePath;
return output;
}
public string CustomScriptPath()
{
var output = string.Empty;
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.js");
if (File.Exists(filePath))
output = filePath;
return output;
}
public string CurrentCulture()
{
return CultureInfo.CurrentCulture.ToString();
}
public string CurrentLanguage()
{
return CultureInfo.InstalledUICulture.Name;
}
public string GetVersion()
{
return Program.Version;
}
public bool VrcClosedGracefully()
{
return LogWatcher.Instance.VrcClosedGracefully;
}
public Dictionary<string, int> GetColourBulk(List<object> userIds)
{
var output = new Dictionary<string, int>();
foreach (string userId in userIds)
{
output.Add(userId, GetColourFromUserID(userId));
}
return output;
}
public void SetAppLauncherSettings(bool enabled, bool killOnExit)
{
AutoAppLaunchManager.Instance.Enabled = enabled;
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
}
public string GetFileBase64(string path)
{
if (File.Exists(path))
{
return Convert.ToBase64String(File.ReadAllBytes(path));
}
return null;
}
}
}

View File

@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using NLog;
namespace VRCX
{
public abstract partial class AppApi
{
// AppApi
public abstract void ShowDevTools();
public abstract void DeleteAllCookies();
public abstract void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand);
public abstract void RefreshVR();
public abstract void RestartVR();
public abstract void SetZoom(double zoomLevel);
public abstract Task<double> GetZoom();
public abstract void DesktopNotification(string BoldText, string Text = "", string Image = "");
public abstract void RestartApplication(bool isUpgrade);
public abstract bool CheckForUpdateExe();
public abstract void ExecuteAppFunction(string function, string json);
public abstract void ExecuteVrFeedFunction(string function, string json);
public abstract void ExecuteVrOverlayFunction(string function, string json);
public abstract string GetLaunchCommand();
public abstract void FocusWindow();
public abstract void ChangeTheme(int value);
public abstract void DoFunny();
public abstract string GetClipboard();
public abstract void SetStartup(bool enabled);
public abstract void CopyImageToClipboard(string path);
public abstract void FlashWindow();
public abstract void SetUserAgent();
public abstract bool IsRunningUnderWine();
// Folders
public abstract string GetVRChatAppDataLocation();
public abstract string GetVRChatPhotosLocation();
public abstract string GetUGCPhotoLocation(string path = "");
public abstract string GetVRChatScreenshotsLocation();
public abstract string GetVRChatCacheLocation();
public abstract bool OpenVrcxAppDataFolder();
public abstract bool OpenVrcAppDataFolder();
public abstract bool OpenVrcPhotosFolder();
public abstract bool OpenUGCPhotosFolder(string ugcPath = "");
public abstract bool OpenVrcScreenshotsFolder();
public abstract bool OpenCrashVrcCrashDumps();
public abstract void OpenShortcutFolder();
public abstract void OpenFolderAndSelectItem(string path, bool isFolder = false);
public abstract Task<string> OpenFolderSelectorDialog(string defaultPath = "");
public abstract Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "",
string defaultFilter = "All files (*.*)|*.*");
// GameHandler
public abstract void OnProcessStateChanged(MonitoredProcess monitoredProcess);
public abstract void CheckGameRunning();
public abstract bool IsGameRunning();
public abstract bool IsSteamVRRunning();
public abstract int QuitGame();
public abstract bool StartGame(string arguments);
public abstract bool StartGameFromPath(string path, string arguments);
// RegistryPlayerPrefs
public abstract object GetVRChatRegistryKey(string key);
public abstract string GetVRChatRegistryKeyString(string key);
public abstract bool SetVRChatRegistryKey(string key, object value, int typeInt);
public abstract void SetVRChatRegistryKey(string key, byte[] value);
public abstract Dictionary<string, Dictionary<string, object>> GetVRChatRegistry();
public abstract void SetVRChatRegistry(string json);
public abstract bool HasVRChatRegistryFolder();
public abstract void DeleteVRChatRegistryFolder();
public abstract string ReadVrcRegJsonFile(string filepath);
// Screenshot
public abstract string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false);
}
}

View File

@@ -0,0 +1,237 @@
using System;
using System.Drawing;
using System.IO;
using System.Threading.Tasks;
namespace VRCX
{
public partial class AppApi
{
public async Task<string> GetImage(string url, string fileId, string version)
{
return await ImageCache.GetImage(url, fileId, version);
}
public string ResizeImageToFitLimits(string base64data)
{
return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false));
}
public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000)
{
using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream);
// for APNG, check if image is png format and less than maxSize
if ((!matchingDimensions || image.Width == image.Height) &&
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) &&
imageData.Length < maxSize &&
image.Width <= maxWidth &&
image.Height <= maxHeight)
{
return imageData;
}
if (image.Width > maxWidth)
{
var sizingFactor = image.Width / (double)maxWidth;
var newHeight = (int)Math.Round(image.Height / sizingFactor);
image = new Bitmap(image, maxWidth, newHeight);
}
if (image.Height > maxHeight)
{
var sizingFactor = image.Height / (double)maxHeight;
var newWidth = (int)Math.Round(image.Width / sizingFactor);
image = new Bitmap(image, newWidth, maxHeight);
}
if (matchingDimensions && image.Width != image.Height)
{
var newSize = Math.Max(image.Width, image.Height);
var newImage = new Bitmap(newSize, newSize);
using var graphics = Graphics.FromImage(newImage);
graphics.Clear(Color.Transparent);
graphics.DrawImage(image, new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height));
image.Dispose();
image = newImage;
}
SaveToFileToUpload();
for (int i = 0; i < 250 && imageData.Length > maxSize; i++)
{
SaveToFileToUpload();
if (imageData.Length < maxSize)
break;
int newWidth;
int newHeight;
if (image.Width > image.Height)
{
newWidth = image.Width - 25;
newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth));
}
else
{
newHeight = image.Height - 25;
newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight));
}
image = new Bitmap(image, newWidth, newHeight);
}
if (imageData.Length > maxSize)
{
throw new Exception("Failed to get image into target filesize.");
}
return imageData;
void SaveToFileToUpload()
{
using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
imageData = imageSaveMemoryStream.ToArray();
}
}
public byte[] ResizePrintImage(byte[] imageData)
{
const int desiredWidth = 1920;
const int desiredHeight = 1080;
using var fileMemoryStream = new MemoryStream(imageData);
var image = new Bitmap(fileMemoryStream);
if (image.Height > image.Width)
image.RotateFlip(RotateFlipType.Rotate90FlipNone);
// increase size to 1920x1080
if (image.Width < desiredWidth || image.Height < desiredHeight)
{
var newHeight = image.Height;
var newWidth = image.Width;
if (image.Width < desiredWidth)
{
var testHeight = (int)Math.Round(image.Height / (image.Width / (double)desiredWidth));
if (testHeight <= desiredHeight)
{
newWidth = desiredWidth;
newHeight = testHeight;
}
}
if (image.Height < desiredHeight)
{
var testWidth = (int)Math.Round(image.Width / (image.Height / (double)desiredHeight));
if (testWidth <= desiredWidth)
{
newHeight = desiredHeight;
newWidth = testWidth;
}
}
var resizedImage = new Bitmap(desiredWidth, desiredHeight);
using var graphics1 = Graphics.FromImage(resizedImage);
graphics1.Clear(Color.White);
var x = (desiredWidth - newWidth) / 2;
var y = (desiredHeight - newHeight) / 2;
graphics1.DrawImage(image, new Rectangle(x, y, newWidth, newHeight));
image.Dispose();
image = resizedImage;
}
// limit size to 1920x1080
if (image.Width > desiredWidth)
{
var sizingFactor = image.Width / (double)desiredWidth;
var newHeight = (int)Math.Round(image.Height / sizingFactor);
image = new Bitmap(image, desiredWidth, newHeight);
}
if (image.Height > desiredHeight)
{
var sizingFactor = image.Height / (double)desiredHeight;
var newWidth = (int)Math.Round(image.Width / sizingFactor);
image = new Bitmap(image, newWidth, desiredHeight);
}
// add white border
// wtf are these magic numbers
const int xOffset = 64; // 2048 / 32
const int yOffset = 69; // 1440 / 20.869
var newImage = new Bitmap(2048, 1440);
using var graphics = Graphics.FromImage(newImage);
graphics.Clear(Color.White);
// graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
var newX = (2048 - image.Width) / 2;
var newY = yOffset;
graphics.DrawImage(image, new Rectangle(newX, newY, image.Width, image.Height));
image.Dispose();
image = newImage;
using var imageSaveMemoryStream = new MemoryStream();
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
return imageSaveMemoryStream.ToArray();
}
public async Task CropAllPrints(string ugcFolderPath)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints");
var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories);
foreach (var file in files)
{
await CropPrintImage(file);
}
}
public async Task<bool> CropPrintImage(string path)
{
var tempPath = path + ".temp";
var bytes = await File.ReadAllBytesAsync(path);
var ms = new MemoryStream(bytes);
Bitmap print = new Bitmap(ms);
// validation step to ensure image is actually a print
if (print.Width != 2048 || print.Height != 1440)
{
return false;
}
var point = new Point(64, 69);
var size = new Size(1920, 1080);
var rectangle = new Rectangle(point, size);
Bitmap cropped = print.Clone(rectangle, print.PixelFormat);
cropped.Save(tempPath);
if (ScreenshotHelper.HasTXt(path))
{
var success = ScreenshotHelper.CopyTXt(path, tempPath);
if (!success)
{
File.Delete(tempPath);
return false;
}
}
File.Move(tempPath, path, true);
return true;
}
public async Task<string> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return null;
var success = await ImageCache.SaveImageToFile(url, filePath);
return success ? filePath : null;
}
public async Task<string> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
{
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder));
Directory.CreateDirectory(folder);
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
if (File.Exists(filePath))
return null;
var success = await ImageCache.SaveImageToFile(url, filePath);
return success ? filePath : null;
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace VRCX
{
public partial class AppApi
{
public Dictionary<string, short> GetVRChatModerations(string currentUserId)
{
// 004 = hideAvatar
// 005 = showAvatar
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
if (!File.Exists(filePath))
return null;
var output = new Dictionary<string, short>();
using var reader = new StreamReader(filePath);
string line;
while ((line = reader.ReadLine()) != null)
{
var index = line.IndexOf(' ');
if (index <= 0)
continue;
var userId = line.Substring(0, index);
var type = short.Parse(line.Substring(line.Length - 3));
output.Add(userId, type);
}
return output;
}
public short GetVRChatUserModeration(string currentUserId, string userId)
{
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
if (!File.Exists(filePath))
return 0;
using var reader = new StreamReader(filePath);
string line;
while ((line = reader.ReadLine()) != null)
{
var index = line.IndexOf(' ');
if (index <= 0)
continue;
if (userId == line.Substring(0, index))
{
return short.Parse(line.Substring(line.Length - 3));
}
}
return 0;
}
public bool SetVRChatUserModeration(string currentUserId, string userId, int type)
{
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
if (!File.Exists(filePath))
return false;
var lines = File.ReadAllLines(filePath).ToList();
var index = lines.FindIndex(x => x.StartsWith(userId));
if (index >= 0)
lines.RemoveAt(index);
if (type != 0)
{
var sb = new StringBuilder(userId);
while (sb.Length < 64)
sb.Append(' ');
sb.Append(type.ToString("000"));
lines.Add(sb.ToString());
}
try
{
File.WriteAllLines(filePath, lines);
}
catch (Exception)
{
return false;
}
return true;
}
}
}

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text.Json;
using Websocket.Client;
namespace VRCX
@@ -14,7 +15,7 @@ namespace VRCX
private static readonly object _ovrtLock = new();
private static WebsocketClient _ovrtWebsocketClient;
private static void Initialize()
private static void InitializeOvrTk()
{
lock (_ovrtLock)
{
@@ -62,16 +63,16 @@ namespace VRCX
private static void SendMessages(IEnumerable<OvrtMessage> ovrtMessages)
{
if(ovrtMessages != null && ovrtMessages.Any())
if (ovrtMessages != null && ovrtMessages.Any())
{
if (_ovrtWebsocketClient == null)
Initialize();
InitializeOvrTk();
if (_ovrtWebsocketClient.IsRunning)
{
foreach (var message in ovrtMessages)
{
_ovrtWebsocketClient.Send(System.Text.Json.JsonSerializer.Serialize(message));
_ovrtWebsocketClient.Send(JsonSerializer.Serialize(message));
}
}
}
@@ -107,7 +108,7 @@ namespace VRCX
messages.Add(new OvrtMessage
{
MessageType = "SendWristNotification",
Json = System.Text.Json.JsonSerializer.Serialize(new OvrtWristNotificationMessage
Json = JsonSerializer.Serialize(new OvrtWristNotificationMessage
{
Body = title + " - " + body
})
@@ -119,7 +120,7 @@ namespace VRCX
messages.Add(new OvrtMessage
{
MessageType = "SendNotification",
Json = System.Text.Json.JsonSerializer.Serialize(new OvrtHudNotificationMessage
Json = JsonSerializer.Serialize(new OvrtHudNotificationMessage
{
Title = title,
Body = body,
@@ -130,6 +131,7 @@ namespace VRCX
SendMessages(messages);
}
private struct OvrtMessage
{
[System.Text.Json.Serialization.JsonPropertyName("messageType")]

View File

@@ -2,41 +2,15 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace VRCX
namespace VRCX;
public partial class AppApi
{
public partial class AppApi
{
/// <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 or not to rename the screenshot file to include the world ID.</param>
public 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;
}
public string GetExtraScreenshotData(string path, bool carouselCache)
{
var fileName = Path.GetFileNameWithoutExtension(path);
@@ -75,15 +49,12 @@ namespace VRCX
return metadata.ToString(Formatting.Indented);
}
/// <summary>
/// Retrieves metadata from a PNG screenshot file and send the result to displayScreenshotMetadata in app.js
/// </summary>
/// <param name="path">The path to the PNG screenshot file.</param>
public string GetScreenshotMetadata(string path)
{
if (string.IsNullOrEmpty(path))
return null;
var metadata = ScreenshotHelper.GetScreenshotMetadata(path);
if (metadata == null)
@@ -139,9 +110,6 @@ namespace VRCX
return json.ToString();
}
/// <summary>
/// Gets and returns the path of the last screenshot taken by VRChat.
/// </summary>
public string GetLastScreenshot()
{
// Get the last screenshot taken by VRChat
@@ -159,5 +127,4 @@ namespace VRCX
return lastScreenshot;
}
}
}

View File

@@ -0,0 +1,21 @@
using System.Threading.Tasks;
namespace VRCX;
public partial class AppApi
{
public async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize)
{
await Update.DownloadUpdate(fileUrl, fileName, hashUrl, downloadSize);
}
public void CancelUpdate()
{
Update.CancelUpdate();
}
public int CheckUpdateProgress()
{
return Update.UpdateProgress;
}
}

View File

@@ -0,0 +1,23 @@
using System.IO;
using System.Text.RegularExpressions;
namespace VRCX;
public partial class AppApi
{
private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
Regex.Escape(new string(Path.GetInvalidPathChars()))));
private static readonly Regex _fileRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
Regex.Escape(new string(Path.GetInvalidFileNameChars()))));
private static string MakeValidFileName(string name)
{
name = name.Replace("/", "");
name = name.Replace("\\", "");
name = _folderRegex.Replace(name, "");
name = _fileRegex.Replace(name, "");
return name;
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.IO;
namespace VRCX
{
public partial class AppApi
{
public string ReadConfigFile()
{
var path = GetVRChatAppDataLocation();
var configFile = Path.Combine(path, "config.json");
if (!Directory.Exists(path) || !File.Exists(configFile))
{
return string.Empty;
}
var json = File.ReadAllText(configFile);
return json;
}
public void WriteConfigFile(string json)
{
var path = GetVRChatAppDataLocation();
var configFile = Path.Combine(path, "config.json");
File.WriteAllText(configFile, json);
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Net;
using System.Net.Sockets;
using System.Text.Json;
namespace VRCX
{
@@ -45,7 +46,7 @@ namespace VRCX
icon = icon
};
var byteBuffer = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(msg);
var byteBuffer = JsonSerializer.SerializeToUtf8Bytes(msg);
broadcastSocket.SendTo(byteBuffer, endPoint);
broadcastSocket.Close();
}

View File

@@ -0,0 +1,140 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using NLog;
namespace VRCX
{
public partial class AppApiElectron : AppApi
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public override void DeleteAllCookies()
{
}
public override void ShowDevTools()
{
}
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
{
}
public override void RefreshVR()
{
}
public override void RestartVR()
{
}
public override void SetZoom(double zoomLevel)
{
}
public override async Task<double> GetZoom()
{
return 1;
}
public override void DesktopNotification(string BoldText, string Text = "", string Image = "")
{
}
public override void RestartApplication(bool isUpgrade)
{
}
public override bool CheckForUpdateExe()
{
return false;
}
public override void ExecuteAppFunction(string function, string json)
{
}
public override void ExecuteVrFeedFunction(string function, string json)
{
}
public override void ExecuteVrOverlayFunction(string function, string json)
{
}
public override string GetLaunchCommand()
{
return string.Empty;
}
public override void FocusWindow()
{
}
public override void ChangeTheme(int value)
{
}
public override void DoFunny()
{
}
public override string GetClipboard()
{
return string.Empty;
}
public override void SetStartup(bool enabled)
{
}
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;
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "xclip",
Arguments = $"-selection clipboard -t image/png -i \"{path}\"",
UseShellExecute = false,
CreateNoWindow = true
}
};
try
{
process.Start();
process.WaitForExit();
}
catch (Exception ex)
{
logger.Error($"Failed to copy image to clipboard: {ex.Message}");
}
}
public override void FlashWindow()
{
}
public override void SetUserAgent()
{
}
public override bool IsRunningUnderWine()
{
return false;
}
}
}

View File

@@ -0,0 +1,196 @@
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 AppApiElectron
{
public static string _homeDirectory;
public static string _steamPath;
public static string _steamUserdataPath;
public static string _vrcPrefixPath;
public static string _vrcAppDataPath;
static AppApiElectron()
{
_homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
_steamPath = Path.Combine(_homeDirectory, ".local/share/Steam");
var flatpakSteamPath = Path.Combine(_homeDirectory, ".var/app/com.valvesoftware.Steam/.local/share/Steam");
if (!Directory.Exists(_steamPath) && Directory.Exists(flatpakSteamPath))
{
logger.Info("Flatpak Steam detected.");
_steamPath = flatpakSteamPath;
}
_steamUserdataPath = Path.Combine(_homeDirectory, ".steam/steam/userdata");
_vrcPrefixPath = Path.Combine(_steamPath, "steamapps/compatdata/438100/pfx");
_vrcAppDataPath = Path.Combine(_vrcPrefixPath, "drive_c/users/steamuser/AppData/LocalLow/VRChat/VRChat");
}
public override string GetVRChatAppDataLocation()
{
return _vrcAppDataPath;
}
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()
{
return Path.Combine(_vrcPrefixPath, "drive_c/users/steamuser/Pictures/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()
{
// TODO: Fix Steam userdata path, for now just get the first folder
if (Directory.Exists(_steamUserdataPath))
{
var steamUserDirs = Directory.GetDirectories(_steamUserdataPath);
if (steamUserDirs.Length > 0)
{
return steamUserDirs[0];
}
}
return string.Empty;
}
public override string GetVRChatScreenshotsLocation()
{
// program files steam userdata screenshots
return Path.Combine(_steamUserdataPath, "760/remote/438100/screenshots");
}
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 = _vrcAppDataPath;
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()
{
// TODO: get path
return false;
}
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);
if (!File.Exists(path) && !Directory.Exists(path))
return;
Process.Start("xdg-open", path);
}
public override async Task<string> OpenFolderSelectorDialog(string defaultPath = "")
{
// TODO: Implement
return string.Empty;
}
public override async Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "",
string defaultFilter = "All files (*.*)|*.*")
{
// TODO: Implement
return string.Empty;
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Diagnostics;
using System.IO;
namespace VRCX
{
public partial class AppApiElectron
{
public override void OnProcessStateChanged(MonitoredProcess monitoredProcess)
{
// unused
}
/// <summary>
/// Checks if the VRChat game and SteamVR are currently running and updates the browser's JavaScript function $app.updateIsGameRunning with the results.
/// </summary>
public override void CheckGameRunning()
{
var isGameRunning = false;
var isSteamVRRunning = false;
if (ProcessMonitor.Instance.IsProcessRunning("VRChat"))
{
isGameRunning = true;
}
if (ProcessMonitor.Instance.IsProcessRunning("vrserver"))
{
isSteamVRRunning = true;
}
}
public override bool IsGameRunning()
{
var isGameRunning = false;
var processes = Process.GetProcesses();
foreach (var process in processes)
{
if (process.ProcessName == "VRChat.exe")
{
isGameRunning = true;
break;
}
}
return isGameRunning;
}
public override bool IsSteamVRRunning()
{
var isSteamVRRunning = false;
var processes = Process.GetProcesses();
foreach (var process in processes)
{
if (process.ProcessName == "vrmonitor" || process.ProcessName == "monado-service")
{
isSteamVRRunning = true;
break;
}
}
return isSteamVRRunning;
}
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
{
var steamPath = _steamPath;
if (string.IsNullOrEmpty(steamPath))
{
logger.Error("Steam path could not be determined.");
return false;
}
var steamExecutable = Path.Combine(steamPath, "steam.sh");
if (!File.Exists(steamExecutable))
{
logger.Error("Steam executable not found.");
return false;
}
Process.Start(new ProcessStartInfo
{
FileName = steamExecutable,
Arguments = $"-applaunch 438100 {arguments}",
UseShellExecute = false,
})?.Close();
return true;
}
catch (Exception ex)
{
logger.Error($"Failed to start VRChat: {ex.Message}");
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;
}
}
}

View File

@@ -0,0 +1,787 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Win32;
using NLog;
namespace VRCX
{
public partial class AppApiElectron
{
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;
}
private static int FindMatchingBracket(string content, int openBracketIndex)
{
int depth = 0;
for (int i = openBracketIndex; i < content.Length; i++)
{
if (content[i] == '{')
depth++;
else if (content[i] == '}')
{
depth--;
if (depth == 0)
return i;
}
}
return -1;
}
private static Dictionary<string, string> ExtractCompatToolMapping(string vdfContent)
{
var compatToolMapping = new Dictionary<string, string>();
const string sectionHeader = "\"CompatToolMapping\"";
int sectionStart = vdfContent.IndexOf(sectionHeader);
if (sectionStart == -1)
{
logger.Error("CompatToolMapping not found");
return compatToolMapping;
}
int blockStart = vdfContent.IndexOf("{", sectionStart) + 1;
int blockEnd = FindMatchingBracket(vdfContent, blockStart - 1);
if (blockStart == -1 || blockEnd == -1)
{
logger.Error("CompatToolMapping block not found");
return compatToolMapping;
}
string blockContent = vdfContent.Substring(blockStart, blockEnd - blockStart);
var keyValuePattern = new Regex("\"(\\d+)\"\\s*\\{[^}]*\"name\"\\s*\"([^\"]+)\"",
RegexOptions.Multiline);
var matches = keyValuePattern.Matches(blockContent);
foreach (Match match in matches)
{
string key = match.Groups[1].Value;
string name = match.Groups[2].Value;
if (key != "0")
{
compatToolMapping[key] = name;
}
}
return compatToolMapping;
}
private static string GetSteamVdfCompatTool()
{
string steamPath = _steamPath;
string configVdfPath = Path.Combine(steamPath, "config", "config.vdf");
if (!File.Exists(configVdfPath))
{
logger.Error("config.vdf not found");
return null;
}
string vdfContent = File.ReadAllText(configVdfPath);
var compatToolMapping = ExtractCompatToolMapping(vdfContent);
if (compatToolMapping.TryGetValue("438100", out string name))
{
return name;
}
return null;
}
private string ParseWineRegOutput(string output, string keyName)
{
if (string.IsNullOrEmpty(output))
return null;
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line =>
!string.IsNullOrWhiteSpace(line) &&
!line.Contains("fixme:") &&
!line.Contains("wine:"))
.ToArray();
foreach (var line in lines)
{
var parts = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim())
.ToArray();
if (parts.Length >= 3 && parts[0].Contains(keyName))
{
var valueType = parts[parts.Length - 2];
var value = parts[parts.Length - 1];
switch (valueType)
{
case "REG_BINARY":
try
{
// Treat the value as a plain hex string and decode it to ASCII
var hexValues = Enumerable.Range(0, value.Length / 2)
.Select(i => value.Substring(i * 2, 2)) // Break string into chunks of 2
.Select(hex => Convert.ToByte(hex, 16)) // Convert each chunk to a byte
.ToArray();
return Encoding.ASCII.GetString(hexValues).TrimEnd('\0');
}
catch (Exception ex)
{
logger.Error($"Error parsing REG_BINARY as plain hex string: {ex.Message}");
return null;
}
case "REG_DWORD":
return "REG_DWORD";
default:
logger.Error($"Unsupported parsed registry value type: {valueType}");
return null;
}
}
}
return null;
}
private string ParseWineRegOutputEx(string output, string keyName)
{
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
string currentKey = null;
string currentValue = null;
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i].Trim();
if (line.Contains("="))
{
var parts = line.Split(new[] { '=' }, 2);
currentKey = parts[0].Trim();
currentValue = parts[1].Trim();
string escapedString = @$"{currentValue}";
escapedString = escapedString.Replace("\\", "");
currentValue = escapedString;
if (currentKey.Contains(keyName))
{
if (currentValue.EndsWith(",\\"))
{
var multiLineValue = new StringBuilder(currentValue.TrimEnd('\\'));
while (currentValue.EndsWith(",\\"))
{
currentValue = lines[++i].Trim();
multiLineValue.Append(currentValue.TrimEnd('\\'));
}
currentValue = multiLineValue.ToString();
}
if (currentValue.StartsWith("dword:"))
{
return int.Parse(currentValue.Substring(6), System.Globalization.NumberStyles.HexNumber).ToString();
}
else if (currentValue.StartsWith("hex:"))
{
var hexValues = currentValue.Substring(4).Replace("\\", "").Split(',');
var bytes = hexValues.Select(hex => Convert.ToByte(hex, 16)).ToArray();
var decodedString = Encoding.UTF8.GetString(bytes);
if (decodedString.StartsWith("[") && decodedString.EndsWith("]"))
{
try
{
var jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject(decodedString);
return Newtonsoft.Json.JsonConvert.SerializeObject(jsonObject, Newtonsoft.Json.Formatting.Indented);
}
catch (Exception ex)
{
logger.Error($"Error parsing JSON: {ex.Message}");
return decodedString;
}
}
else
{
return currentValue;
}
}
else
{
return currentValue;
}
}
}
}
logger.Error($"Key not found: {keyName}");
return null;
}
public static string GetVRChatWinePath()
{
string compatTool = GetSteamVdfCompatTool();
if (compatTool == null)
{
logger.Error("CompatTool not found");
return null;
}
string steamPath = _steamPath;
string steamAppsCommonPath = Path.Combine(steamPath, "steamapps", "common");
string compatabilityToolsPath = Path.Combine(steamPath, "compatibilitytools.d");
string protonPath = Path.Combine(steamAppsCommonPath, compatTool);
string compatToolPath = Path.Combine(compatabilityToolsPath, compatTool);
string winePath = "";
if (Directory.Exists(compatToolPath))
{
winePath = Path.Combine(compatToolPath, "files", "bin", "wine");
if (!File.Exists(winePath))
{
Console.WriteLine("Wine not found in CompatTool path");
return null;
}
}
else if (Directory.Exists(protonPath))
{
winePath = Path.Combine(protonPath, "dist", "bin", "wine");
if (!File.Exists(winePath))
{
logger.Error("Wine not found in Proton path");
return null;
}
}
else if (Directory.Exists(compatabilityToolsPath))
{
var dirs = Directory.GetDirectories(compatabilityToolsPath);
foreach (var dir in dirs)
{
if (dir.Contains(compatTool))
{
winePath = Path.Combine(dir, "files", "bin", "wine");
if (File.Exists(winePath))
{
break;
}
}
}
if (!File.Exists(winePath))
{
Console.WriteLine("Wine not found in CompatTool path");
return null;
}
}
if (winePath == "")
{
logger.Error("CompatTool and Proton not found");
return null;
}
return winePath;
}
private ProcessStartInfo GetWineProcessStartInfo(string winePath, string winePrefix, string wineCommand)
{
var processStartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"{wineCommand.Replace("\"", "\\\"")}\"",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = Encoding.UTF8,
StandardErrorEncoding = Encoding.UTF8
};
processStartInfo.Environment["WINEFSYNC"] = "1";
processStartInfo.Environment["WINEPREFIX"] = winePrefix;
//processStartInfo.Environment["WINEDEBUG"] = "-all";
return processStartInfo;
}
private string GetWineRegCommand(string command)
{
string winePath = GetVRChatWinePath();
string winePrefix = _vrcPrefixPath;
string wineRegCommand = $"\"{winePath}\" reg {command}";
ProcessStartInfo processStartInfo = GetWineProcessStartInfo(winePath, winePrefix, wineRegCommand);
using var process = Process.Start(processStartInfo);
string output = process.StandardOutput.ReadToEnd();
string error = process.StandardError.ReadToEnd();
process.WaitForExit();
if (!string.IsNullOrEmpty(error) &&
!error.Contains("wineserver: using server-side synchronization.") &&
!error.Contains("fixme:wineusb:query_id"))
{
logger.Error($"Wine reg command error: {error}");
return null;
}
return output;
}
private string GetWineRegCommandEx(string regCommand)
{
string winePrefix = _vrcPrefixPath;
string filePath = Path.Combine(winePrefix, "user.reg");
if (!File.Exists(filePath))
throw new FileNotFoundException($"Registry file not found at {filePath}");
var match = Regex.Match(regCommand, @"^(add|query|delete)\s+""([^""]+)""(?:\s+/v\s+""([^""]+)"")?(?:\s+/t\s+(\w+))?(?:\s+/d\s+([^\s]+))?.*$");
if (!match.Success)
throw new ArgumentException("Invalid command format.");
string action = match.Groups[1].Value.ToLower();
string valueName = match.Groups[3].Success ? match.Groups[3].Value : null;
string valueType = match.Groups[4].Success ? match.Groups[4].Value : null;
string valueData = match.Groups[5].Success ? match.Groups[5].Value : null;
var lines = File.ReadAllLines(filePath).ToList();
var updatedLines = new List<string>();
bool keyFound = false;
bool valueFound = false;
bool inVRChatSection = false;
int headerEndIndex = -1;
string keyHeader = "[Software\\\\VRChat\\\\VRChat]";
if (action == "add")
{
for (int i = 0; i < lines.Count; i++)
{
string line = lines[i].Trim();
if (line.StartsWith(keyHeader))
{
inVRChatSection = true;
keyFound = true;
headerEndIndex = i;
// Add header and metadata lines
while (i < lines.Count && (lines[i].StartsWith("#") || lines[i].StartsWith("@") || lines[i].Trim().StartsWith(keyHeader)))
{
updatedLines.Add(lines[i]);
i++;
}
i--;
continue;
}
else if (inVRChatSection && line.StartsWith("["))
{
inVRChatSection = false;
}
if (inVRChatSection && valueName != null)
{
if (line.TrimStart().StartsWith($"\"{valueName}\"="))
{
valueFound = true;
updatedLines.Add($"\"{valueName}\"={GetRegistryValueFormat(valueType, valueData)}");
continue;
}
}
updatedLines.Add(lines[i]);
}
// Add new value if not found but section exists
if (keyFound && !valueFound && valueName != null)
{
var insertIndex = headerEndIndex + 2;
while (insertIndex < updatedLines.Count &&
(updatedLines[insertIndex].StartsWith("#") || updatedLines[insertIndex].StartsWith("@")))
{
insertIndex++;
}
updatedLines.Insert(insertIndex, $"\"{valueName}\"={GetRegistryValueFormat(valueType, valueData)}");
}
File.WriteAllLines(filePath, updatedLines);
return $"Command '{regCommand}' executed successfully.";
}
else if (action == "query")
{
if (!valueName.Contains("_h"))
{
valueName = AddHashToKeyName(valueName);
}
foreach (var line in lines)
{
if (line.Contains(valueName))
{
return line;
}
}
return $"Value \"{valueName}\" not found.";
}
logger.Error($"Unsupported registry command: {regCommand}");
return $"Command '{regCommand}' executed successfully.";
}
private static string GetRegistryValueFormat(string valueType, string valueData)
{
if (valueType?.ToUpper() == "REG_DWORD100")
{
double inputValue = double.Parse(valueData);
Span<byte> dataBytes = stackalloc byte[sizeof(double)];
BitConverter.TryWriteBytes(dataBytes, inputValue);
var hexValues = dataBytes.ToArray().Select(b => b.ToString("X2")).ToArray();
var byteString = string.Join(",", hexValues).ToLower();
var result = $"hex(4):{byteString}";
return result;
}
return valueType?.ToUpper() switch
{
"REG_DWORD" => $"dword:{int.Parse(valueData):X8}",
_ => throw new ArgumentException($"Unsupported registry value type: {valueType}"),
};
}
public override object GetVRChatRegistryKey(string key)
{
try
{
key = AddHashToKeyName(key);
string regCommand = $"query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"{key}\"";
var queryResult = GetWineRegCommand(regCommand);
if (queryResult == null)
return null;
var result = ParseWineRegOutput(queryResult, key);
if (result == "REG_DWORD")
{
queryResult = GetWineRegCommandEx(regCommand);
result = ParseWineRegOutputEx(queryResult, key);
}
return result;
}
catch (Exception ex)
{
logger.Error($"Exception in GetRegistryValueFromWine: {ex.Message}");
return null;
}
}
public override string GetVRChatRegistryKeyString(string key)
{
// for electron
return GetVRChatRegistryKey(key)?.ToString();
}
// TODO: check this
public async Task SetVRChatRegistryKeyAsync(string key, object value, int typeInt)
{
await Task.Run(() =>
{
SetVRChatRegistryKey(key, value, typeInt);
});
}
public override bool SetVRChatRegistryKey(string key, object value, int typeInt)
{
var type = (RegistryValueKind)typeInt;
var keyName = AddHashToKeyName(key);
switch (type)
{
case RegistryValueKind.Binary:
if (value is JsonElement jsonElement)
{
if (jsonElement.ValueKind == JsonValueKind.String)
{
byte[] byteArray = Encoding.UTF8.GetBytes(jsonElement.GetString());
var data = BitConverter.ToString(byteArray).Replace("-", "");
if (data.Length == 0)
data = "\"\"";
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + data + " /f";
var addResult = GetWineRegCommand(regCommand);
if (addResult == null)
return false;
}
else if (jsonElement.ValueKind == JsonValueKind.Array)
{
byte[] byteArray = jsonElement.EnumerateArray()
.Select(e => (byte)e.GetInt32()) // Convert each element to byte
.ToArray();
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + BitConverter.ToString(byteArray).Replace("-", "") + " /f";
var addResult = GetWineRegCommand(regCommand);
if (addResult == null)
return false;
}
else
{
logger.Error($"Invalid value for REG_BINARY: {value}. It must be a JSON string or array.");
return false;
}
}
else if (value is string jsonArray)
{
byte[] byteArray = Encoding.UTF8.GetBytes(jsonArray);
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + BitConverter.ToString(byteArray).Replace("-", "") + " /f";
var addResult = GetWineRegCommand(regCommand);
if (addResult == null)
return false;
}
else
{
logger.Error($"Invalid value for REG_BINARY: {value}. It must be a JsonElement.");
return false;
}
break;
case RegistryValueKind.DWord:
if (value is int intValue)
{
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + intValue + " /f";
var addResult = GetWineRegCommandEx(regCommand);
if (addResult == null)
return false;
}
else if (value is string stringValue && int.TryParse(stringValue, out int parsedIntValue))
{
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + parsedIntValue + " /f";
var addResult = GetWineRegCommandEx(regCommand);
if (addResult == null)
return false;
}
else if (value is JsonElement jsonElementValue && jsonElementValue.ValueKind == JsonValueKind.Number)
{
int parsedInt32Value = jsonElementValue.GetInt32();
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + parsedInt32Value + " /f";
var addResult = GetWineRegCommandEx(regCommand);
if (addResult == null)
return false;
}
else
{
logger.Error($"Invalid value for REG_DWORD: {value}. It must be a valid integer.");
return false;
}
break;
default:
logger.Error($"Unsupported set registry value type: {typeInt}");
return false;
}
return true;
}
public override void SetVRChatRegistryKey(string key, byte[] value)
{
var keyName = AddHashToKeyName(key);
var data = BitConverter.ToString(value).Replace("-", "");
if (data.Length == 0)
data = "\"\"";
var regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + data + " /f";
GetWineRegCommand(regCommand);
}
public override Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
{
return null;
}
// TODO: no object type
// public Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
public string GetVRChatRegistryJson()
{
var registry = new Dictionary<string, Dictionary<string, object>>();
string regCommand = "query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\"";
var queryResult = GetWineRegCommand(regCommand);
if (queryResult == null)
return null;
var lines = queryResult.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Where(line =>
!string.IsNullOrWhiteSpace(line) &&
!line.Contains("fixme:") &&
!line.Contains("wine:"))
.ToArray();
foreach (var line in lines)
{
var parts = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim())
.ToArray();
if (parts.Length >= 3)
{
var keyName = parts[0];
var index = keyName.LastIndexOf("_h", StringComparison.Ordinal);
if (index > 0)
keyName = keyName.Substring(0, index);
var valueType = parts[parts.Length - 2];
var value = parts[parts.Length - 1];
switch (valueType)
{
case "REG_BINARY":
try
{
// Treat the value as a plain hex string and decode it to ASCII
var hexValues = Enumerable.Range(0, value.Length / 2)
.Select(i => value.Substring(i * 2, 2)) // Break string into chunks of 2
.Select(hex => Convert.ToByte(hex, 16)) // Convert each chunk to a byte
.ToArray();
var binDict = new Dictionary<string, object>
{
{ "data", Encoding.ASCII.GetString(hexValues).TrimEnd('\0') },
{ "type", 3 }
};
registry.Add(keyName, binDict);
}
catch (Exception ex)
{
logger.Error($"Error parsing REG_BINARY as plain hex string: {ex.Message}");
}
break;
case "REG_DWORD":
string regCommandExDword = $"query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"{keyName}\"";
var queryResultExDword = GetWineRegCommandEx(regCommandExDword);
if (queryResultExDword == null)
break;
var resultExDword = ParseWineRegOutputEx(queryResultExDword, keyName);
if (resultExDword == null)
break;
try
{
if (resultExDword.StartsWith("hex(4)"))
{
string hexString = resultExDword;
string[] hexValues = hexString.Split(':')[1].Split(',');
byte[] byteValues = hexValues.Select(h => Convert.ToByte(h, 16)).ToArray();
if (byteValues.Length != 8)
{
throw new ArgumentException("Input does not represent a valid 8-byte double-precision float.");
}
double parsedDouble = BitConverter.ToDouble(byteValues, 0);
var doubleDict = new Dictionary<string, object>
{
{ "data", parsedDouble },
{ "type", 100 } // it's special
};
registry.Add(keyName, doubleDict);
}
else
{
// Convert dword value to integer
int parsedInt = int.Parse(resultExDword);
var dwordDict = new Dictionary<string, object>
{
{ "data", parsedInt },
{ "type", 4 }
};
registry.Add(keyName, dwordDict);
}
}
catch (Exception ex)
{
logger.Error($"Error parsing REG_DWORD: {ex.Message}");
}
break;
}
}
}
return Newtonsoft.Json.JsonConvert.SerializeObject(registry, Newtonsoft.Json.Formatting.Indented);
}
public override void SetVRChatRegistry(string json)
{
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"]);
string keyName = AddHashToKeyName(item.Key);
if (type == 4)
{
int intValue = data.GetInt32();
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + intValue + " /f";
var addResult = GetWineRegCommandEx(regCommand);
if (addResult == null)
continue;
}
else if (type == 100)
{
var valueLong = data.GetDouble();
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD100 /d " + valueLong + " /f";
var addResult = GetWineRegCommandEx(regCommand);
if (addResult == null)
continue;
}
else
{
// This slows down the recovery process but using async can be problematic
if (data.ValueKind == JsonValueKind.Number)
{
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()
{
string regCommand = "query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\"";
var queryResult = GetWineRegCommand(regCommand);
if (queryResult == null)
return false;
return !string.IsNullOrEmpty(queryResult);
}
private void CreateVRChatRegistryFolder()
{
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /f";
GetWineRegCommand(regCommand);
}
public override void DeleteVRChatRegistryFolder()
{
string regCommand = "delete \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /f";
GetWineRegCommand(regCommand);
}
public override string ReadVrcRegJsonFile(string filepath)
{
if (!File.Exists(filepath))
return string.Empty;
var json = File.ReadAllText(filepath);
return json;
}
}
}

View File

@@ -0,0 +1,37 @@
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 AppApiElectron
{
public override string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
{
var winePrefix = Path.Combine(_vrcPrefixPath, "/drive_c/");
var winePath = path.Substring(3).Replace("\\", "/");
path = Path.Combine(winePrefix, winePath);
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;
}
}
}

View File

@@ -1,231 +0,0 @@
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 AppApi
{
private void OnProcessStateChanged(MonitoredProcess monitoredProcess)
{
if (!monitoredProcess.HasName("VRChat") && !monitoredProcess.HasName("vrserver"))
return;
CheckGameRunning();
}
/// <summary>
/// Checks if the VRChat game and SteamVR are currently running and updates the browser's JavaScript function $app.updateIsGameRunning with the results.
/// </summary>
public 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);
}
/// <summary>
/// Kills the VRChat process if it is currently running.
/// </summary>
/// <returns>The number of processes that were killed (0 or 1).</returns>
public int QuitGame()
{
var processes = Process.GetProcessesByName("vrchat");
if (processes.Length == 1)
processes[0].Kill();
return processes.Length;
}
/// <summary>
/// Kills the install.exe process after exiting game.
/// </summary>
/// <returns>Whether the process is killed (true or false).</returns>
public bool KillInstall()
{
bool isSuccess = false;
var processes = Process.GetProcessesByName("install");
foreach (var p in processes)
{
// "E:\SteamLibrary\steamapps\common\VRChat\install.exe"
var match = Regex.Match(GetProcessName(p.Id), "(.+?\\\\VRChat.*)(!?\\\\install.exe)");
if (match.Success)
{
// Sometimes install.exe is suspended
ResumeProcess(p.Id);
p.Kill();
isSuccess = true;
break;
}
}
return isSuccess;
}
[DllImport("ntdll.dll")]
private static extern uint NtResumeProcess([In] IntPtr processHandle);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, [Out, MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpExeName, ref uint lpdwSize);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint processAccess, bool inheritHandle, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle([In] IntPtr handle);
public static void ResumeProcess(int processId)
{
IntPtr hProc = IntPtr.Zero;
try
{
// Gets the handle to the Process
// 0x800 mean required to suspend or resume a process.
hProc = OpenProcess(0x800, false, processId);
if (hProc != IntPtr.Zero)
NtResumeProcess(hProc);
}
finally
{
// close handle.
if (hProc != IntPtr.Zero)
CloseHandle(hProc);
}
}
public static string GetProcessName(int pid)
{
IntPtr hProc = IntPtr.Zero;
try
{
// 0x400 mean required to retrieve certain information about a process, such as its token, exit code, and priority class.
// 0x10 mean required to read memory in a process using ReadProcessMemory.
hProc = OpenProcess(0x400 | 0x10, false, pid);
if (hProc != IntPtr.Zero)
{
int lengthSb = 4000;
uint lpSize = 65535;
var sb = new StringBuilder(lengthSb);
string result = String.Empty;
if (QueryFullProcessImageName(hProc, 0, sb, ref lpSize))
{
result = sb.ToString();
}
return result;
}
}
finally
{
if (hProc != IntPtr.Zero)
CloseHandle(hProc);
}
return String.Empty;
}
/// <summary>
/// Starts the VRChat game process with the specified command-line arguments.
/// </summary>
/// <param name="arguments">The command-line arguments to pass to the VRChat game.</param>
public 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;
}
/// <summary>
/// Starts the VRChat game process with the specified command-line arguments from the given path.
/// </summary>
/// <param name="path">The path to the VRChat game executable.</param>
/// <param name="arguments">The command-line arguments to pass to the VRChat game.</param>
/// <returns>True if the game was started successfully, false otherwise.</returns>
public 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;
}
}
}

View File

@@ -1,114 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace VRCX
{
public partial class AppApi
{
/// <summary>
/// Retrieves a dictionary of moderations for the specified user from the VRChat LocalPlayerModerations folder.
/// </summary>
/// <param name="currentUserId">The ID of the current user.</param>
/// <returns>A dictionary of moderations for the specified user, or null if the file does not exist.</returns>
public Dictionary<string, short> GetVRChatModerations(string currentUserId)
{
// 004 = hideAvatar
// 005 = showAvatar
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
if (!File.Exists(filePath))
return null;
var output = new Dictionary<string, short>();
using (var reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var index = line.IndexOf(' ');
if (index <= 0)
continue;
var userId = line.Substring(0, index);
var type = short.Parse(line.Substring(line.Length - 3));
output.Add(userId, type);
}
}
return output;
}
/// <summary>
/// Retrieves the moderation type for the specified user from the VRChat LocalPlayerModerations folder.
/// </summary>
/// <param name="currentUserId">The ID of the current user.</param>
/// <param name="userId">The ID of the user to retrieve the moderation type for.</param>
/// <returns>The moderation type for the specified user, or 0 if the file does not exist or the user is not found.</returns>
public short GetVRChatUserModeration(string currentUserId, string userId)
{
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
if (!File.Exists(filePath))
return 0;
using (var reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
var index = line.IndexOf(' ');
if (index <= 0)
continue;
if (userId == line.Substring(0, index))
{
return short.Parse(line.Substring(line.Length - 3));
}
}
}
return 0;
}
/// <summary>
/// Sets the moderation type for the specified user in the VRChat LocalPlayerModerations folder.
/// </summary>
/// <param name="currentUserId">The ID of the current user.</param>
/// <param name="userId">The ID of the user to set the moderation type for.</param>
/// <param name="type">The moderation type to set for the specified user.</param>
/// <returns>True if the operation was successful, false otherwise.</returns>
public bool SetVRChatUserModeration(string currentUserId, string userId, int type)
{
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
if (!File.Exists(filePath))
return false;
var lines = File.ReadAllLines(filePath).ToList();
var index = lines.FindIndex(x => x.StartsWith(userId));
if (index >= 0)
lines.RemoveAt(index);
if (type != 0)
{
var sb = new StringBuilder(userId);
while (sb.Length < 64)
sb.Append(' ');
sb.Append(type.ToString("000"));
lines.Add(sb.ToString());
}
try
{
File.WriteAllLines(filePath, lines);
}
catch (Exception)
{
return false;
}
return true;
}
}
}

View File

@@ -1,36 +0,0 @@
using System;
using System.IO;
namespace VRCX
{
public partial class AppApi
{
/// <summary>
/// Reads the VRChat config file and returns its contents as a string.
/// </summary>
/// <returns>The contents of the VRChat config file as a string, or an empty string if the file does not exist.</returns>
public string ReadConfigFile()
{
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
var configFile = Path.Combine(logPath, "config.json");
if (!Directory.Exists(logPath) || !File.Exists(configFile))
{
return string.Empty;
}
var json = File.ReadAllText(configFile);
return json;
}
/// <summary>
/// Writes the specified JSON string to the VRChat config file.
/// </summary>
/// <param name="json">The JSON string to write to the config file.</param>
public void WriteConfigFile(string json)
{
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
var configFile = Path.Combine(logPath, "config.json");
File.WriteAllText(configFile, json);
}
}
}