mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-25 01:33:51 +02:00
Electron support for Linux (#1074)
* init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Fix UI var * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * Test updater * Rebase and handle merge conflicts * Fix Linux updater * Fix Linux app restart * Fix friend order * Handle AppImageInstaller, show an install message on Linux * Updates to the AppImage installer * Fix Linux updater, fix set version, check for .NET, copy wine prefix * Handle random errors * Rotate tall prints * try fix Linux restart bug * Final --------- Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
This commit is contained in:
140
Dotnet/AppApi/Electron/AppApiElectron.cs
Normal file
140
Dotnet/AppApi/Electron/AppApiElectron.cs
Normal 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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
196
Dotnet/AppApi/Electron/Folders.cs
Normal file
196
Dotnet/AppApi/Electron/Folders.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Dotnet/AppApi/Electron/GameHandler.cs
Normal file
123
Dotnet/AppApi/Electron/GameHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
787
Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs
Normal file
787
Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Dotnet/AppApi/Electron/Screenshot.cs
Normal file
37
Dotnet/AppApi/Electron/Screenshot.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user