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 ExtractCompatToolMapping(string vdfContent) { var compatToolMapping = new Dictionary(); const string sectionHeader = "\"CompatToolMapping\""; int sectionStart = vdfContent.IndexOf(sectionHeader, StringComparison.Ordinal); 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.Join(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.Join(steamPath, "steamapps", "common"); string compatabilityToolsPath = Path.Join(steamPath, "compatibilitytools.d"); string protonPath = Path.Join(steamAppsCommonPath, compatTool); string compatToolPath = Path.Join(compatabilityToolsPath, compatTool); string winePath = ""; if (Directory.Exists(compatToolPath)) { winePath = Path.Join(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.Join(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.Join(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; if (string.IsNullOrEmpty(winePath) || string.IsNullOrEmpty(winePrefix)) { logger.Info("VRC Wine path was not found"); return null; } 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.Join(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(); 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 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> GetVRChatRegistry() { return null; } // TODO: no object type // public Dictionary> GetVRChatRegistry() public string GetVRChatRegistryJson() { var registry = new Dictionary>(); const string regCommand = "query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\""; var queryResult = GetWineRegCommand(regCommand); if (queryResult == null) throw new Exception("Failed to get VRC registry data"); 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 { { "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 { { "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 { { "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>>(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; } } }