diff --git a/Dotnet/AppApi/RegistryPlayerPrefs.cs b/Dotnet/AppApi/RegistryPlayerPrefs.cs
new file mode 100644
index 00000000..99e1761d
--- /dev/null
+++ b/Dotnet/AppApi/RegistryPlayerPrefs.cs
@@ -0,0 +1,312 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.Json;
+using System.Threading;
+using System.Windows.Forms;
+using Microsoft.Win32;
+
+namespace VRCX
+{
+ public partial class AppApi
+ {
+ [DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
+ public static extern int RegSetValueExA(
+ IntPtr hKey,
+ string lpValueName,
+ int reserved,
+ RegistryValueKind dwType,
+ byte[] lpData,
+ int cbData
+ );
+
+ [DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
+ public static extern int RegOpenKeyExA(
+ IntPtr hKey,
+ string lpSubKey,
+ int ulOptions,
+ int samDesired,
+ out IntPtr phkResult
+ );
+
+ [DllImport("advapi32.dll")]
+ public static extern int RegCloseKey(IntPtr hKey);
+
+ public 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;
+ }
+
+ ///
+ /// Retrieves the value of the specified key from the VRChat group in the windows registry.
+ ///
+ /// The name of the key to retrieve.
+ /// The value of the specified key, or null if the key does not exist.
+ public object GetVRChatRegistryKey(string key)
+ {
+ var keyName = AddHashToKeyName(key);
+ using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
+ {
+ var data = regKey?.GetValue(keyName);
+ if (data == null)
+ return null;
+
+ var type = regKey.GetValueKind(keyName);
+ switch (type)
+ {
+ case RegistryValueKind.Binary:
+ return Encoding.ASCII.GetString((byte[])data);
+
+ case RegistryValueKind.DWord:
+ if (data.GetType() != typeof(long))
+ return data;
+
+ long.TryParse(data.ToString(), out var longValue);
+ var bytes = BitConverter.GetBytes(longValue);
+ var doubleValue = BitConverter.ToDouble(bytes, 0);
+ return doubleValue;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Sets the value of the specified key in the VRChat group in the windows registry.
+ ///
+ /// The name of the key to set.
+ /// The value to set for the specified key.
+ /// The RegistryValueKind type.
+ /// True if the key was successfully set, false otherwise.
+ public bool SetVRChatRegistryKey(string key, object value, int typeInt)
+ {
+ var type = (RegistryValueKind)typeInt;
+ var keyName = AddHashToKeyName(key);
+ using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat", true))
+ {
+ if (regKey == null)
+ return false;
+
+ object setValue = null;
+ switch (type)
+ {
+ case RegistryValueKind.Binary:
+ setValue = Encoding.ASCII.GetBytes(value.ToString());
+ break;
+
+ case RegistryValueKind.DWord:
+ setValue = value;
+ break;
+ }
+
+ if (setValue == null)
+ return false;
+
+ regKey.SetValue(keyName, setValue, type);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Sets the value of the specified key in the VRChat group in the windows registry.
+ ///
+ /// The name of the key to set.
+ /// The value to set for the specified key.
+ public void SetVRChatRegistryKey(string key, byte[] value)
+ {
+ var keyName = AddHashToKeyName(key);
+ var hKey = (IntPtr)0x80000001; // HKEY_LOCAL_MACHINE
+ const int keyWrite = 0x20006;
+ const string keyFolder = @"SOFTWARE\VRChat\VRChat";
+ var openKeyResult = RegOpenKeyExA(hKey, keyFolder, 0, keyWrite, out var folderPointer);
+ if (openKeyResult != 0)
+ throw new Exception("Error opening registry key. Error code: " + openKeyResult);
+
+ var setKeyResult = RegSetValueExA(folderPointer, keyName, 0, RegistryValueKind.DWord, value, value.Length);
+ if (setKeyResult != 0)
+ throw new Exception("Error setting registry value. Error code: " + setKeyResult);
+
+ RegCloseKey(hKey);
+ }
+
+ public Dictionary> GetVRChatRegistry()
+ {
+ var output = new Dictionary>();
+ using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
+ {
+ if (regKey == null)
+ throw new Exception("Nothing to backup.");
+
+ var keys = regKey.GetValueNames();
+ foreach (var key in keys)
+ {
+ var data = regKey.GetValue(key);
+ var index = key.LastIndexOf("_h", StringComparison.Ordinal);
+ if (index <= 0)
+ continue;
+
+ var keyName = key.Substring(0, index);
+ if (data == null)
+ continue;
+
+ var type = regKey.GetValueKind(key);
+ switch (type)
+ {
+ case RegistryValueKind.Binary:
+ var binDict = new Dictionary
+ {
+ { "data", Encoding.ASCII.GetString((byte[])data) },
+ { "type", type }
+ };
+ output.Add(keyName, binDict);
+ break;
+
+ case RegistryValueKind.DWord:
+ if (data.GetType() != typeof(long))
+ {
+ var dwordDict = new Dictionary
+ {
+ { "data", data },
+ { "type", type }
+ };
+ output.Add(keyName, dwordDict);
+ break;
+ }
+
+ Span spanLong = stackalloc long[] { (long)data };
+ var doubleValue = MemoryMarshal.Cast(spanLong)[0];
+ var floatDict = new Dictionary
+ {
+ { "data", doubleValue },
+ { "type", 100 } // it's special
+ };
+ output.Add(keyName, floatDict);
+ break;
+
+ default:
+ Debug.WriteLine($"Unknown registry value kind: {type}");
+ break;
+ }
+ }
+ }
+ return output;
+ }
+
+ public void SetVRChatRegistry(string json)
+ {
+ CreateVRChatRegistryFolder();
+ Span spanDouble = stackalloc double[1];
+ var dict = System.Text.Json.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"]);
+
+ if (data.ValueKind == JsonValueKind.Number)
+ {
+ if (type == 100)
+ {
+ // fun handling of double to long to byte array
+ spanDouble[0] = data.Deserialize();
+ var valueLong = MemoryMarshal.Cast(spanDouble)[0];
+ const int dataLength = sizeof(long);
+ var dataBytes = new byte[dataLength];
+ Buffer.BlockCopy(BitConverter.GetBytes(valueLong), 0, dataBytes, 0, dataLength);
+ SetVRChatRegistryKey(item.Key, dataBytes);
+ continue;
+ }
+
+ if (int.TryParse(data.ToString(), out var intValue))
+ {
+ SetVRChatRegistryKey(item.Key, intValue, type);
+ continue;
+ }
+
+ throw new Exception("Unknown number type: " + item.Key);
+ }
+
+ SetVRChatRegistryKey(item.Key, data, type);
+ }
+ }
+
+ public bool HasVRChatRegistryFolder()
+ {
+ using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
+ {
+ return regKey != null;
+ }
+ }
+
+ public void CreateVRChatRegistryFolder()
+ {
+ if (HasVRChatRegistryFolder())
+ return;
+
+ using (var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\VRChat\VRChat"))
+ {
+ if (key == null)
+ throw new Exception("Error creating registry key.");
+ }
+ }
+
+ public void DeleteVRChatRegistryFolder()
+ {
+ using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
+ {
+ if (regKey == null)
+ return;
+
+ Registry.CurrentUser.DeleteSubKeyTree(@"SOFTWARE\VRChat\VRChat");
+ }
+ }
+
+ ///
+ /// Opens a file dialog to select a VRChat registry backup JSON file.
+ ///
+ public void OpenVrcRegJsonFileDialog()
+ {
+ if (dialogOpen) return;
+ dialogOpen = true;
+
+ var thread = new Thread(() =>
+ {
+ using (var openFileDialog = new OpenFileDialog())
+ {
+ openFileDialog.DefaultExt = ".json";
+ openFileDialog.Filter = "JSON Files (*.json)|*.json";
+ openFileDialog.FilterIndex = 1;
+ openFileDialog.RestoreDirectory = true;
+
+ if (openFileDialog.ShowDialog() != DialogResult.OK)
+ {
+ dialogOpen = false;
+ return;
+ }
+
+ dialogOpen = false;
+
+ var path = openFileDialog.FileName;
+ if (string.IsNullOrEmpty(path))
+ return;
+
+ // return file contents
+ var json = File.ReadAllText(path);
+ ExecuteAppFunction("restoreVrcRegistryFromFile", json);
+ }
+ });
+
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ }
+ }
+}
\ No newline at end of file
diff --git a/VRCX.csproj b/VRCX.csproj
index bb766201..a980618d 100644
--- a/VRCX.csproj
+++ b/VRCX.csproj
@@ -1,4 +1,4 @@
-
+
@@ -86,6 +86,7 @@
+
diff --git a/html/src/app.js b/html/src/app.js
index 49531caf..10fdae2f 100644
--- a/html/src/app.js
+++ b/html/src/app.js
@@ -5267,6 +5267,7 @@ speechSynthesis.getVoices();
this.refreshCustomCss();
this.refreshCustomScript();
this.checkVRChatDebugLogging();
+ this.checkAutoBackupRestoreVrcRegistry();
this.migrateStoredUsers();
this.$nextTick(function () {
this.$el.style.display = '';
@@ -14553,6 +14554,16 @@ speechSynthesis.getVoices();
this.autoStateChange
);
};
+ $app.data.vrcRegistryAutoBackup = configRepository.getBool(
+ 'VRCX_vrcRegistryAutoBackup',
+ true
+ );
+ $app.methods.saveVrcRegistryAutoBackup = function () {
+ configRepository.setBool(
+ 'VRCX_vrcRegistryAutoBackup',
+ this.vrcRegistryAutoBackup
+ );
+ };
$app.data.orderFriendsGroup0 = configRepository.getBool(
'orderFriendGroup0',
true
@@ -23850,11 +23861,14 @@ speechSynthesis.getVoices();
var D = this.VRCXUpdateDialog;
var url = this.branches[this.branch].urlReleases;
this.checkingForVRCXUpdate = true;
- var response = await webApiService.execute({
- url,
- method: 'GET'
- });
- this.checkingForVRCXUpdate = false;
+ try {
+ var response = await webApiService.execute({
+ url,
+ method: 'GET'
+ });
+ } finally {
+ this.checkingForVRCXUpdate = false;
+ }
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
@@ -23919,12 +23933,15 @@ speechSynthesis.getVoices();
}
var url = this.branches[this.branch].urlLatest;
this.checkingForVRCXUpdate = true;
- var response = await webApiService.execute({
- url,
- method: 'GET'
- });
+ try {
+ var response = await webApiService.execute({
+ url,
+ method: 'GET'
+ });
+ } finally {
+ this.checkingForVRCXUpdate = false;
+ }
this.pendingVRCXUpdate = false;
- this.checkingForVRCXUpdate = false;
var json = JSON.parse(response.data);
if (this.debugWebRequests) {
console.log(json, response);
@@ -27667,7 +27684,6 @@ speechSynthesis.getVoices();
return false;
});
};
-
// group members
@@ -28441,6 +28457,268 @@ speechSynthesis.getVoices();
});
};
+ // #endregion
+ // #region | Dialog: registry backup dialog
+
+ $app.data.registryBackupDialog = {
+ visible: false
+ };
+
+ $app.data.registryBackupTable = {
+ data: [],
+ tableProps: {
+ stripe: true,
+ size: 'mini',
+ defaultSort: {
+ prop: 'date',
+ order: 'descending'
+ }
+ },
+ layout: 'table'
+ };
+
+ $app.methods.showRegistryBackupDialog = function () {
+ this.$nextTick(() =>
+ adjustDialogZ(this.$refs.registryBackupDialog.$el)
+ );
+ var D = this.registryBackupDialog;
+ D.visible = true;
+ this.updateRegistryBackupDialog();
+ };
+
+ $app.methods.updateRegistryBackupDialog = function () {
+ var D = this.registryBackupDialog;
+ this.registryBackupTable.data = [];
+ if (!D.visible) {
+ return;
+ }
+ var backupsJson = configRepository.getString(
+ 'VRCX_VRChatRegistryBackups'
+ );
+ if (!backupsJson) {
+ backupsJson = JSON.stringify([]);
+ }
+ this.registryBackupTable.data = JSON.parse(backupsJson);
+ };
+
+ $app.methods.promptVrcRegistryBackupName = async function () {
+ var name = await this.$prompt(
+ 'Enter a name for the backup',
+ 'Backup Name',
+ {
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
+ inputPattern: /\S+/,
+ inputErrorMessage: 'Name is required',
+ inputValue: 'Backup'
+ }
+ );
+ if (name.action === 'confirm') {
+ this.backupVrcRegistry(name.value);
+ }
+ };
+
+ $app.methods.backupVrcRegistry = async function (name) {
+ var regJson = await AppApi.GetVRChatRegistry();
+ var newBackup = {
+ name,
+ date: new Date().toJSON(),
+ data: regJson
+ };
+ var backupsJson = configRepository.getString(
+ 'VRCX_VRChatRegistryBackups'
+ );
+ if (!backupsJson) {
+ backupsJson = JSON.stringify([]);
+ }
+ var backups = JSON.parse(backupsJson);
+ backups.push(newBackup);
+ configRepository.setString(
+ 'VRCX_VRChatRegistryBackups',
+ JSON.stringify(backups)
+ );
+ this.updateRegistryBackupDialog();
+ };
+
+ $app.methods.deleteVrcRegistryBackup = function (row) {
+ var backups = this.registryBackupTable.data;
+ removeFromArray(backups, row);
+ configRepository.setString(
+ 'VRCX_VRChatRegistryBackups',
+ JSON.stringify(backups)
+ );
+ this.updateRegistryBackupDialog();
+ };
+
+ $app.methods.restoreVrcRegistryBackup = function (row) {
+ this.$confirm('Continue? Restore Backup', 'Confirm', {
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
+ type: 'warning',
+ callback: (action) => {
+ if (action !== 'confirm') {
+ return;
+ }
+ var data = JSON.stringify(row.data);
+ AppApi.SetVRChatRegistry(data)
+ .then(() => {
+ this.$message({
+ message: 'VRC registry settings restored',
+ type: 'success'
+ });
+ })
+ .catch((e) => {
+ console.error(e);
+ this.$message({
+ message: `Failed to restore VRC registry settings, check console for full error: ${e}`,
+ type: 'error'
+ });
+ });
+ }
+ });
+ };
+
+ $app.methods.saveVrcRegistryBackupToFile = function (row) {
+ this.downloadAndSaveJson(row.name, row.data);
+ };
+
+ $app.methods.restoreVrcRegistryFromFile = function (json) {
+ try {
+ var data = JSON.parse(json);
+ if (!data || typeof data !== 'object') {
+ throw new Error('Invalid JSON');
+ }
+ // quick check to make sure it's a valid registry backup
+ for (var key in data) {
+ var value = data[key];
+ if (
+ typeof value !== 'object' ||
+ typeof value.type !== 'number' ||
+ typeof value.data === 'undefined'
+ ) {
+ throw new Error('Invalid JSON');
+ }
+ }
+ AppApi.SetVRChatRegistry(json)
+ .then(() => {
+ this.$message({
+ message: 'VRC registry settings restored',
+ type: 'success'
+ });
+ })
+ .catch((e) => {
+ console.error(e);
+ this.$message({
+ message: `Failed to restore VRC registry settings, check console for full error: ${e}`,
+ type: 'error'
+ });
+ });
+ } catch {
+ this.$message({
+ message: 'Invalid JSON',
+ type: 'error'
+ });
+ }
+ };
+
+ $app.methods.deleteVrcRegistry = function () {
+ this.$confirm('Continue? Delete VRC Registry Settings', 'Confirm', {
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
+ type: 'warning',
+ callback: (action) => {
+ if (action !== 'confirm') {
+ return;
+ }
+ AppApi.DeleteVRChatRegistryFolder().then(() => {
+ this.$message({
+ message: 'VRC registry settings deleted',
+ type: 'success'
+ });
+ });
+ }
+ });
+ };
+
+ $app.methods.clearVrcRegistryDialog = function () {
+ this.registryBackupTable.data = [];
+ };
+
+ $app.methods.checkAutoBackupRestoreVrcRegistry = async function () {
+ if (!this.vrcRegistryAutoBackup) {
+ return;
+ }
+
+ // check for auto restore
+ var hasVRChatRegistryFolder = await AppApi.HasVRChatRegistryFolder();
+ if (!hasVRChatRegistryFolder) {
+ var lastBackupDate = configRepository.getString(
+ 'VRCX_VRChatRegistryLastBackupDate'
+ );
+ var lastRestoreCheck = configRepository.getString(
+ 'VRCX_VRChatRegistryLastRestoreCheck'
+ );
+ if (
+ lastRestoreCheck &&
+ lastBackupDate &&
+ lastRestoreCheck === lastBackupDate
+ ) {
+ // only ask to restore once
+ return;
+ }
+ // popup message about auto restore
+ this.$alert(
+ $t('dialog.registry_backup.restore_prompt'),
+ $t('dialog.registry_backup.header')
+ );
+ this.showRegistryBackupDialog();
+ AppApi.FocusWindow();
+ configRepository.setString(
+ 'VRCX_VRChatRegistryLastRestoreCheck',
+ lastBackupDate
+ );
+ } else {
+ this.autoBackupVrcRegistry();
+ }
+ };
+
+ $app.methods.autoBackupVrcRegistry = function () {
+ var date = new Date();
+ var lastBackupDate = configRepository.getString(
+ 'VRCX_VRChatRegistryLastBackupDate'
+ );
+ if (lastBackupDate) {
+ var lastBackup = new Date(lastBackupDate);
+ var diff = date.getTime() - lastBackup.getTime();
+ var diffDays = Math.floor(diff / (1000 * 60 * 60 * 24));
+ if (diffDays < 7) {
+ return;
+ }
+ }
+ var backupsJson = configRepository.getString(
+ 'VRCX_VRChatRegistryBackups'
+ );
+ if (!backupsJson) {
+ backupsJson = JSON.stringify([]);
+ }
+ var backups = JSON.parse(backupsJson);
+ backups.forEach((backup) => {
+ if (backup.name === 'Auto Backup') {
+ // remove old auto backup
+ removeFromArray(backups, backup);
+ }
+ });
+ configRepository.setString(
+ 'VRCX_VRChatRegistryBackups',
+ JSON.stringify(backups)
+ );
+ this.backupVrcRegistry('Auto Backup');
+ configRepository.setString(
+ 'VRCX_VRChatRegistryLastBackupDate',
+ date.toJSON()
+ );
+ };
+
// #endregion
$app = new Vue($app);
diff --git a/html/src/index.pug b/html/src/index.pug
index 9545a9ee..d94794d5 100644
--- a/html/src/index.pug
+++ b/html/src/index.pug
@@ -2761,6 +2761,29 @@ html
el-button(type="default" size="mini" icon="el-icon-download" circle @click="downloadAndSaveImage(fullscreenImageDialog.imageUrl)" style="margin-left:5px")
img(v-lazy="fullscreenImageDialog.imageUrl" style="width:100%;height:100vh;object-fit:contain")
+ el-dialog.x-dialog(:before-close="beforeDialogClose" @closed="clearVrcRegistryDialog" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="registryBackupDialog" :visible.sync="registryBackupDialog.visible" :title="$t('dialog.registry_backup.header')" width="600px")
+ div(v-if="registryBackupDialog.visible" style="margin-top:10px")
+ div.options-container
+ div.options-container-item
+ span.name {{ $t('dialog.registry_backup.auto_backup') }}
+ el-switch(v-model="vrcRegistryAutoBackup" @change="saveVrcRegistryAutoBackup")
+ el-button(@click="promptVrcRegistryBackupName" size="small") {{ $t('dialog.registry_backup.backup') }}
+ el-button(@click="AppApi.OpenVrcRegJsonFileDialog()" size="small") {{ $t('dialog.registry_backup.restore_from_file') }}
+ el-button(@click="deleteVrcRegistry" size="small") {{ $t('dialog.registry_backup.reset') }}
+ data-tables(v-bind="registryBackupTable" style="margin-top:10px")
+ el-table-column(:label="$t('dialog.registry_backup.name')" prop="name")
+ el-table-column(:label="$t('dialog.registry_backup.date')" prop="date")
+ template(v-once #default="scope")
+ span {{ scope.row.date | formatDate('long') }}
+ el-table-column(:label="$t('dialog.registry_backup.action')" width="90" align="right")
+ template(v-once #default="scope")
+ el-tooltip(placement="top" :content="$t('dialog.registry_backup.restore')" :disabled="hideTooltips")
+ el-button(type="text" icon="el-icon-upload2" size="mini" @click="restoreVrcRegistryBackup(scope.row)")
+ el-tooltip(placement="top" :content="$t('dialog.registry_backup.save_to_file')" :disabled="hideTooltips")
+ el-button(type="text" icon="el-icon-download" size="mini" @click="saveVrcRegistryBackupToFile(scope.row)")
+ el-tooltip(placement="top" :content="$t('dialog.registry_backup.delete')" :disabled="hideTooltips")
+ el-button(type="text" icon="el-icon-delete" size="mini" @click="deleteVrcRegistryBackup(scope.row)")
+
//- dialog: open source software notice
el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" :visible.sync="ossDialog" :title="$t('dialog.open_source.header')" width="650px")
div(v-if="ossDialog" style="height:350px;overflow:hidden scroll;word-break:break-all")
diff --git a/html/src/localization/en/en.json b/html/src/localization/en/en.json
index 78c7be8c..cbc9ddc2 100644
--- a/html/src/localization/en/en.json
+++ b/html/src/localization/en/en.json
@@ -379,6 +379,7 @@
"header": "Advanced",
"launch_options": "Launch Options",
"screenshot_metadata": "Screenshot Metadata",
+ "vrc_registry_backup": "VRC Registry Backup",
"common_folders": "Common Folders",
"pending_offline": {
"header": "Pending Offline",
@@ -1230,6 +1231,20 @@
"copy_image": "Copy Image",
"open_folder": "Open Folder",
"upload": "Upload"
+ },
+ "registry_backup": {
+ "header": "VRC Registry Settings Backup",
+ "backup": "Backup",
+ "restore": "Restore",
+ "save_to_file": "Save to file",
+ "delete": "Delete",
+ "restore_from_file": "Restore from file",
+ "reset": "Reset",
+ "name": "Name",
+ "date": "Date",
+ "action": "Action",
+ "auto_backup": "Weekly Auto Backup",
+ "restore_prompt": "VRCX has noticed auto backup of VRC registry settings is enabled but this computer dosn't have any, if you'd like to restore from backup you can do so from here."
}
},
"prompt": {
diff --git a/html/src/mixins/tabs/settings.pug b/html/src/mixins/tabs/settings.pug
index 20718a5c..e4fbcaca 100644
--- a/html/src/mixins/tabs/settings.pug
+++ b/html/src/mixins/tabs/settings.pug
@@ -379,6 +379,7 @@ mixin settingsTab()
el-button(size="small" icon="el-icon-s-operation" @click="showVRChatConfig()") VRChat config.json
el-button(size="small" icon="el-icon-s-operation" @click="showLaunchOptions()") {{ $t('view.settings.advanced.advanced.launch_options') }}
el-button(size="small" icon="el-icon-picture" @click="showScreenshotMetadataDialog()") {{ $t('view.settings.advanced.advanced.screenshot_metadata') }}
+ el-button(size="small" icon="el-icon-goods" @click="showRegistryBackupDialog()") {{ $t('view.settings.advanced.advanced.vrc_registry_backup') }}
//- Advanced | Common Folders
div.options-container
span.header {{ $t('view.settings.advanced.advanced.common_folders') }}