Add systems for local world persistence (#553)

* chore: Change vscode workspace settings to work with omnisharp

* refactor(.NET): Use connection string builder to init sqlite database

* docs(.NET): Add method documentation to most things that matter

* docs(.NET): Add more docs I forgot to commit apparently

* feat: Add PoC world database structure ^& http listener

* fix: Send a response if VRCX isn't initialized rather than hanging

* feat: Initialize world db schema on startup

* feat: Allow worlds to store data in db through logfile

* use existing current location for worldDB

* Add tooltips

* chore: Make it so vscode can format C# files without prettier

* refactor: Add sqlite-net to (eventually) replace sqlite impl

* refactor: Make use of sqlite-net for world database

* docs: Add todo for fixing some random exception

* refactor: Remove now-unused SQLiteWorld

* refactor: Fix DB init query and change table structure again

* refactor: Add WorldDataRequest, add attributes for camelcase json keys

* Support current user location from API in addition to gameLog

* Change current location check for worldDB

* feat: Take store requests in JSON, identify worlds by GUID on store.

* refactor: Remove unused worldId param from connection key generator

* docs: Add more documentation to the methods for the world database

* fix: Hey wait that's not a primary key

* feat: Add a 10MB data cap for worlds shared across all of their rows.

* fix: Don't calculate size of world date twice when inserting

* refactor: Discard the guid variable since we only check for validity

* docs: Add docs/comments for new data cap functionality

* feat: Implement /getbulk API endpoint

* fix: Correct WorldDB init query typo

* fix: Update data entries properly instead of using 'OR REPLACE'

* refactor: Move endpoint processing to separate methods

* refactor: Add another check for error 503, remove old code

* feat: Add debug capability to /vrcx/getbulk

* fix: Correct the usage of getUser in actuallyGetCurrentLocation

* feat: Add store errors, implement external reading, stop 404ing

* docs: Add docs for new world db funcs

* refactor: Change world db server listen port to 22500

* fix: Use getUser correctly, dumb dumb

* fix: This error set shouldn't be here

* feat: Future-proof api endpoints. Add /status endpoint

---------

Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
Teacup
2023-05-31 18:34:50 -04:00
committed by GitHub
parent b06cba0669
commit 0101f3474f
17 changed files with 6193 additions and 147 deletions
+217 -1
View File
@@ -51,6 +51,11 @@ namespace VRCX
CheckGameRunning();
}
/// <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);
@@ -58,6 +63,11 @@ namespace VRCX
return Convert.ToBase64String(md5);
}
/// <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.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
@@ -68,12 +78,21 @@ namespace VRCX
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.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
return fileData.Length.ToString();
}
/// <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\";
@@ -87,6 +106,10 @@ namespace VRCX
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\";
@@ -94,6 +117,11 @@ namespace VRCX
File.WriteAllText(configFile, json);
}
/// <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()
{
var json = ReadConfigFile();
@@ -114,21 +142,34 @@ namespace VRCX
return cachePath;
}
/// <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");
}
/// <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>
/// 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;
@@ -144,10 +185,16 @@ namespace VRCX
isSteamVRRunning = true;
}
// 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.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning);
}
/// <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");
@@ -157,6 +204,10 @@ namespace VRCX
return processes.Length;
}
/// <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 void StartGame(string arguments)
{
// try stream first
@@ -204,6 +255,12 @@ namespace VRCX
}
}
/// <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"))
@@ -222,6 +279,11 @@ namespace VRCX
return true;
}
/// <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://") ||
@@ -264,21 +326,43 @@ namespace VRCX
VRCXVR.Instance.Restart();
}
/// <summary>
/// Returns an array of arrays containing information about the connected VR devices.
/// Each sub-array contains the type of device and its current state
/// </summary>
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
public string[][] GetVRDevices()
{
return VRCXVR.Instance.GetDevices();
}
/// <summary>
/// Returns the current CPU usage as a percentage.
/// </summary>
/// <returns>The current CPU usage as a percentage.</returns>
public float CpuUsage()
{
return CpuMonitor.Instance.CpuUsage;
}
/// <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 string GetImage(string url, string fileId, string version)
{
return 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 = "")
{
var toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText02);
@@ -297,6 +381,13 @@ namespace VRCX
ToastNotificationManager.CreateToastNotifier("VRCX").Show(toast);
}
/// <summary>
/// Displays an XSOverlay notification with the specified title, content, and optional image.
/// </summary>
/// <param name="Title">The title of the notification.</param>
/// <param name="Content">The content of the notification.</param>
/// <param name="Timeout">The duration of the notification in milliseconds.</param>
/// <param name="Image">The optional image to display in the notification.</param>
public void XSNotification(string Title, string Content, int Timeout, string Image = "")
{
bool UseBase64Icon;
@@ -332,6 +423,10 @@ namespace VRCX
broadcastSocket.SendTo(byteBuffer, endPoint);
}
/// <summary>
/// Downloads the VRCX update executable from the specified URL and saves it to the AppData directory.
/// </summary>
/// <param name="url">The URL of the VRCX update to download.</param>
public void DownloadVRCXUpdate(string url)
{
var Location = Path.Combine(Program.AppDataDirectory, "update.exe");
@@ -340,6 +435,9 @@ namespace VRCX
client.DownloadFile(new Uri(url), Location);
}
/// <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()
{
var VRCXProcess = new Process();
@@ -350,6 +448,10 @@ namespace VRCX
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")))
@@ -357,6 +459,9 @@ namespace VRCX
return false;
}
/// <summary>
/// Sends an IPC packet to announce the start of VRCX.
/// </summary>
public void IPCAnnounceStart()
{
IPCServer.Send(new IPCPacket
@@ -365,6 +470,11 @@ namespace VRCX
});
}
/// <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
@@ -397,6 +507,10 @@ namespace VRCX
VRCXVR._browser2.ExecuteScriptAsync($"$app.{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.LaunchCommand;
@@ -404,11 +518,18 @@ namespace VRCX
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;
@@ -418,6 +539,10 @@ namespace VRCX
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;
@@ -442,6 +567,10 @@ namespace VRCX
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;
@@ -457,6 +586,10 @@ namespace VRCX
WinformThemer.DoFunny();
}
/// <summary>
/// Returns the number of milliseconds that the system has been running.
/// </summary>
/// <returns>The number of milliseconds that the system has been running.</returns>
public double GetUptime()
{
using (var uptime = new PerformanceCounter("System", "System Up Time"))
@@ -466,12 +599,23 @@ namespace VRCX
}
}
/// <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>();
@@ -483,6 +627,10 @@ namespace VRCX
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;
@@ -493,6 +641,11 @@ namespace VRCX
return clipboard;
}
/// <summary>
/// Retrieves the value of the specified key from the VRChat group in the windows registry.
/// </summary>
/// <param name="key">The name of the key to retrieve.</param>
/// <returns>The value of the specified key, or null if the key does not exist.</returns>
public object GetVRChatRegistryKey(string key)
{
// https://answers.unity.com/questions/177945/playerprefs-changing-the-name-of-keys.html?childToView=208076#answer-208076
@@ -528,6 +681,12 @@ namespace VRCX
return null;
}
/// <summary>
/// Sets the value of the specified key in the VRChat group in the windows registry.
/// </summary>
/// <param name="key">The name of the key to set.</param>
/// <param name="value">The value to set for the specified key.</param>
/// <returns>True if the key was successfully set, false otherwise.</returns>
public bool SetVRChatRegistryKey(string key, string value)
{
uint hash = 5381;
@@ -562,6 +721,11 @@ namespace VRCX
return true;
}
/// <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)
{
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
@@ -590,6 +754,12 @@ namespace VRCX
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";
@@ -615,6 +785,13 @@ namespace VRCX
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";
@@ -648,6 +825,10 @@ namespace VRCX
return true;
}
/// <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
@@ -679,6 +860,13 @@ namespace VRCX
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
}
/// <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 void AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
{
var fileName = Path.GetFileNameWithoutExtension(path);
@@ -696,7 +884,10 @@ namespace VRCX
ScreenshotHelper.WritePNGDescription(path, metadataString);
}
// Create a function that opens a file dialog so a user can choose a .png file. Print the name of the file after it is chosen
/// <summary>
/// Opens a file dialog to select a PNG screenshot file.
/// The resulting file path is passed to <see cref="GetScreenshotMetadata(string)"/>.
/// </summary>
public void OpenScreenshotFileDialog()
{
if (dialogOpen) return;
@@ -737,6 +928,10 @@ namespace VRCX
thread.Start();
}
/// <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 void GetScreenshotMetadata(string path)
{
if (string.IsNullOrEmpty(path))
@@ -818,6 +1013,9 @@ namespace VRCX
ExecuteAppFunction("displayScreenshotMetadata", metadata.ToString(Formatting.Indented));
}
/// <summary>
/// Gets the last screenshot taken by VRChat and retrieves its metadata.
/// </summary>
public void GetLastScreenshot()
{
// Get the last screenshot taken by VRChat
@@ -836,6 +1034,10 @@ namespace VRCX
GetScreenshotMetadata(lastScreenshot);
}
/// <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
@@ -849,6 +1051,9 @@ namespace VRCX
}
}
/// <summary>
/// Opens the folder containing user-defined shortcuts, if it exists.
/// </summary>
public void OpenShortcutFolder()
{
var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
@@ -858,6 +1063,11 @@ 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)
{
// 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.
@@ -901,11 +1111,17 @@ namespace VRCX
}
}
/// <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())