mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 22:33:50 +02:00
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:
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -3,5 +3,10 @@
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"omnisharp.useModernNet": false,
|
||||
"[csharp]": {
|
||||
"editor.defaultFormatter": "ms-dotnettools.csharp"
|
||||
}
|
||||
}
|
||||
|
||||
218
AppApi.cs
218
AppApi.cs
@@ -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())
|
||||
|
||||
@@ -59,6 +59,12 @@ namespace VRCX
|
||||
return AppApi.Instance.GetVRChatCacheLocation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the full location of the VRChat cache for a specific asset bundle.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the asset bundle.</param>
|
||||
/// <param name="version">The version of the asset bundle.</param>
|
||||
/// <returns>The full location of the VRChat cache for the specified asset bundle.</returns>
|
||||
public string GetVRChatCacheFullLocation(string id, int version)
|
||||
{
|
||||
var cachePath = GetVRChatCacheLocation();
|
||||
@@ -67,6 +73,12 @@ namespace VRCX
|
||||
return Path.Combine(cachePath, idHash, versionLocation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks the VRChat cache for a specific asset bundle.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the asset bundle.</param>
|
||||
/// <param name="version">The version of the asset bundle.</param>
|
||||
/// <returns>An array containing the file size and lock status of the asset bundle.</returns>
|
||||
public long[] CheckVRChatCache(string id, int version)
|
||||
{
|
||||
long FileSize = -1;
|
||||
@@ -155,6 +167,11 @@ namespace VRCX
|
||||
DownloadProgress = -16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the cache directory for a specific asset bundle.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the asset bundle to delete.</param>
|
||||
/// <param name="version">The version of the asset bundle to delete.</param>
|
||||
public void DeleteCache(string id, int version)
|
||||
{
|
||||
var FullLocation = GetVRChatCacheFullLocation(id, version);
|
||||
@@ -162,6 +179,9 @@ namespace VRCX
|
||||
Directory.Delete(FullLocation, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the entire VRChat cache directory.
|
||||
/// </summary>
|
||||
public void DeleteAllCache()
|
||||
{
|
||||
var cachePath = GetVRChatCacheLocation();
|
||||
@@ -172,6 +192,9 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes empty directories from the VRChat cache directory and deletes old versions of cached asset bundles.
|
||||
/// </summary>
|
||||
public void SweepCache()
|
||||
{
|
||||
var cachePath = GetVRChatCacheLocation();
|
||||
@@ -201,6 +224,10 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the size of the VRChat cache directory in bytes.
|
||||
/// </summary>
|
||||
/// <returns>The size of the VRChat cache directory in bytes.</returns>
|
||||
public long GetCacheSize()
|
||||
{
|
||||
var cachePath = GetVRChatCacheLocation();
|
||||
@@ -214,6 +241,12 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Recursively calculates the size of a directory and all its subdirectories.
|
||||
/// </summary>
|
||||
/// <param name="d">The directory to calculate the size of.</param>
|
||||
/// <returns>The size of the directory and all its subdirectories in bytes.</returns>
|
||||
public long DirSize(DirectoryInfo d)
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace VRCX
|
||||
|
||||
private DateTime startTime = DateTime.Now;
|
||||
private Dictionary<string, Process> startedProcesses = new Dictionary<string, Process>();
|
||||
private static readonly byte[] shortcutSignatureBytes = { 0x4C, 0x00, 0x00, 0x00 }; // signature for ShellLinkHeader\
|
||||
private static readonly byte[] shortcutSignatureBytes = { 0x4C, 0x00, 0x00, 0x00 }; // signature for ShellLinkHeader
|
||||
|
||||
private const uint TH32CS_SNAPPROCESS = 2;
|
||||
|
||||
@@ -123,6 +123,11 @@ namespace VRCX
|
||||
|
||||
// This is a recursive function that kills a process and all of its children.
|
||||
// It uses the CreateToolhelp32Snapshot winapi func to get a snapshot of all running processes, loops through them with Process32First/Process32Next, and kills any processes that have the given pid as their parent.
|
||||
|
||||
/// <summary>
|
||||
/// Kills a process and all of its child processes.
|
||||
/// </summary>
|
||||
/// <param name="pid">The process ID of the parent process.</param>
|
||||
public static void KillProcessTree(int pid)
|
||||
{
|
||||
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
|
||||
@@ -14,6 +14,9 @@ using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
/// <summary>
|
||||
/// Monitors the VRChat log files for changes and provides access to the log data.
|
||||
/// </summary>
|
||||
public class LogWatcher
|
||||
{
|
||||
public static readonly LogWatcher Instance;
|
||||
@@ -88,6 +91,9 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the log watcher by checking for new log files and updating the log list.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (m_ResetLog)
|
||||
@@ -157,6 +163,11 @@ namespace VRCX
|
||||
m_FirstRun = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the log file starting from the current position and updates the log context.
|
||||
/// </summary>
|
||||
/// <param name="fileInfo">The file information of the log file to parse.</param>
|
||||
/// <param name="logContext">The log context to update.</param>
|
||||
private void ParseLog(FileInfo fileInfo, LogContext logContext)
|
||||
{
|
||||
try
|
||||
@@ -224,6 +235,7 @@ namespace VRCX
|
||||
ParseLogUsharpVideoPlay(fileInfo, logContext, line, offset) ||
|
||||
ParseLogUsharpVideoSync(fileInfo, logContext, line, offset) ||
|
||||
ParseLogWorldVRCX(fileInfo, logContext, line, offset) ||
|
||||
ParseLogWorldDataVRCX(fileInfo, logContext, line, offset) ||
|
||||
ParseLogOnAudioConfigurationChanged(fileInfo, logContext, line, offset) ||
|
||||
ParseLogScreenshot(fileInfo, logContext, line, offset) ||
|
||||
ParseLogStringDownload(fileInfo, logContext, line, offset) ||
|
||||
@@ -593,6 +605,19 @@ namespace VRCX
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ParseLogWorldDataVRCX(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
||||
{
|
||||
// [VRCX-World] store:test:testvalue
|
||||
|
||||
if (string.Compare(line, offset, "[VRCX-World] ", 0, 13, StringComparison.Ordinal) != 0)
|
||||
return false;
|
||||
|
||||
var data = line.Substring(offset + 13);
|
||||
|
||||
WorldDBManager.Instance.ProcessLogWorldDataRequest(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ParseLogVideoChange(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
||||
{
|
||||
// 2021.04.20 13:37:69 Log - [Video Playback] Attempting to resolve URL 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
|
||||
@@ -927,7 +952,7 @@ namespace VRCX
|
||||
private bool ParseOpenVRInit(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
||||
{
|
||||
// 2022.07.29 02:52:14 Log - OpenVR initialized!
|
||||
|
||||
|
||||
// 2023.04.22 16:52:28 Log - Initializing VRSDK.
|
||||
// 2023.04.22 16:52:29 Log - StartVRSDK: Open VR Loader
|
||||
|
||||
@@ -944,7 +969,7 @@ namespace VRCX
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private bool ParseDesktopMode(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
||||
{
|
||||
// 2023.04.22 16:54:18 Log - VR Disabled
|
||||
|
||||
13
Program.cs
13
Program.cs
@@ -6,6 +6,7 @@
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace VRCX
|
||||
@@ -78,8 +79,12 @@ namespace VRCX
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
// I'll re-do this whole function eventually I swear
|
||||
var worldDBServer = new WorldDBManager("http://127.0.0.1:22500/");
|
||||
Task.Run(worldDBServer.Start);
|
||||
|
||||
ProcessMonitor.Instance.Init();
|
||||
SQLite.Instance.Init();
|
||||
SQLiteLegacy.Instance.Init();
|
||||
VRCXStorage.Load();
|
||||
LoadFromConfig();
|
||||
CpuMonitor.Instance.Init();
|
||||
@@ -99,14 +104,18 @@ namespace VRCX
|
||||
AutoAppLaunchManager.Instance.Exit();
|
||||
LogWatcher.Instance.Exit();
|
||||
WebApi.Instance.Exit();
|
||||
worldDBServer.Stop();
|
||||
|
||||
Discord.Instance.Exit();
|
||||
CpuMonitor.Instance.Exit();
|
||||
VRCXStorage.Save();
|
||||
SQLite.Instance.Exit();
|
||||
SQLiteLegacy.Instance.Exit();
|
||||
ProcessMonitor.Instance.Exit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets GPUFix to true if it is not already set and the VRCX_GPUFix key in the database is true.
|
||||
/// </summary>
|
||||
private static void LoadFromConfig()
|
||||
{
|
||||
if (!GPUFix)
|
||||
|
||||
151
SQLiteLegacy.cs
Normal file
151
SQLiteLegacy.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using CefSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SQLiteLegacy
|
||||
{
|
||||
public static readonly SQLiteLegacy Instance;
|
||||
private readonly ReaderWriterLockSlim m_ConnectionLock;
|
||||
private readonly SQLiteConnection m_Connection;
|
||||
|
||||
static SQLiteLegacy()
|
||||
{
|
||||
Instance = new SQLiteLegacy();
|
||||
}
|
||||
|
||||
public SQLiteLegacy()
|
||||
{
|
||||
m_ConnectionLock = new ReaderWriterLockSlim();
|
||||
|
||||
var dataSource = Program.ConfigLocation;
|
||||
m_Connection = new SQLiteConnection($"Data Source=\"{dataSource}\";Version=3;PRAGMA locking_mode=NORMAL;PRAGMA busy_timeout=5000", true);
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
{
|
||||
m_Connection.Open();
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
{
|
||||
m_Connection.Close();
|
||||
m_Connection.Dispose();
|
||||
}
|
||||
|
||||
public void Execute(IJavascriptCallback callback, string sql, IDictionary<string, object> args = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
m_ConnectionLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
using (var command = new SQLiteCommand(sql, m_Connection))
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
}
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read() == true)
|
||||
{
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(null, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(null, null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_ConnectionLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
callback.Dispose();
|
||||
}
|
||||
|
||||
public void Execute(Action<object[]> callback, string sql, IDictionary<string, object> args = null)
|
||||
{
|
||||
m_ConnectionLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
using (var command = new SQLiteCommand(sql, m_Connection))
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
}
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read() == true)
|
||||
{
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
callback(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_ConnectionLock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public int ExecuteNonQuery(string sql, IDictionary<string, object> args = null)
|
||||
{
|
||||
int result = -1;
|
||||
|
||||
m_ConnectionLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
using (var command = new SQLiteCommand(sql, m_Connection))
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
}
|
||||
result = command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_ConnectionLock.ExitWriteLock();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,11 @@ namespace VRCX
|
||||
return new PNGChunk(type, chunkData);
|
||||
}
|
||||
|
||||
// parse LFS screenshot PNG metadata
|
||||
/// <summary>
|
||||
/// Parses the metadata string of a vrchat screenshot with taken with LFS and returns a JObject containing the parsed data.
|
||||
/// </summary>
|
||||
/// <param name="metadataString">The metadata string to parse.</param>
|
||||
/// <returns>A JObject containing the parsed data.</returns>
|
||||
public static JObject ParseLfsPicture(string metadataString)
|
||||
{
|
||||
var metadata = new JObject();
|
||||
|
||||
3
Util.cs
3
Util.cs
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using CefSharp;
|
||||
|
||||
namespace VRCX
|
||||
@@ -11,7 +12,7 @@ namespace VRCX
|
||||
repository.Register("SharedVariable", SharedVariable.Instance, false);
|
||||
repository.Register("WebApi", WebApi.Instance, true);
|
||||
repository.Register("VRCXStorage", VRCXStorage.Instance, true);
|
||||
repository.Register("SQLite", SQLite.Instance, true);
|
||||
repository.Register("SQLite", SQLiteLegacy.Instance, true);
|
||||
repository.Register("LogWatcher", LogWatcher.Instance, true);
|
||||
repository.Register("Discord", Discord.Instance, true);
|
||||
repository.Register("AssetBundleCacher", AssetBundleCacher.Instance, true);
|
||||
|
||||
11
VRCX.csproj
11
VRCX.csproj
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
@@ -87,11 +87,13 @@
|
||||
<Compile Include="CefCustomDownloadHandler.cs" />
|
||||
<Compile Include="CefCustomDragHandler.cs" />
|
||||
<Compile Include="CefCustomMenuHandler.cs" />
|
||||
<Compile Include="WorldDatabase.cs" />
|
||||
<Compile Include="ImageCache.cs" />
|
||||
<Compile Include="IPCClient.cs" />
|
||||
<Compile Include="IPCServer.cs" />
|
||||
<Compile Include="ProcessMonitor.cs" />
|
||||
<Compile Include="ScreenshotHelper.cs" />
|
||||
<Compile Include="SQLite.cs" />
|
||||
<Compile Include="StartupArgs.cs" />
|
||||
<Compile Include="Update.cs" />
|
||||
<Compile Include="CefService.cs" />
|
||||
@@ -101,7 +103,7 @@
|
||||
<Compile Include="NoopDragHandler.cs" />
|
||||
<Compile Include="OpenVR\openvr_api.cs" />
|
||||
<Compile Include="WebApi.cs" />
|
||||
<Compile Include="SQLite.cs" />
|
||||
<Compile Include="SQLiteLegacy.cs" />
|
||||
<Compile Include="SharedVariable.cs" />
|
||||
<Compile Include="Util.cs" />
|
||||
<Compile Include="VRForm.cs">
|
||||
@@ -123,6 +125,8 @@
|
||||
<Compile Include="AppApi.cs" />
|
||||
<Compile Include="VRCXStorage.cs" />
|
||||
<Compile Include="JsonSerializer.cs" />
|
||||
<Compile Include="WorldDataRequestResponse.cs" />
|
||||
<Compile Include="WorldDBManager.cs" />
|
||||
<Compile Include="WinApi.cs" />
|
||||
<Compile Include="WinformBase.cs">
|
||||
<SubType>Form</SubType>
|
||||
@@ -222,6 +226,7 @@
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent>xcopy /y "$(ProjectDir)OpenVR\win64\openvr_api.dll"</PostBuildEvent>
|
||||
<PostBuildEvent>xcopy /y "$(ProjectDir)OpenVR\win64\openvr_api.dll"
|
||||
xcopy /y "$(ProjectDir)lib\sqlite3.dll"</PostBuildEvent>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -60,8 +60,8 @@ namespace VRCX
|
||||
|
||||
internal void LoadCookies()
|
||||
{
|
||||
SQLite.Instance.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS `cookies` (`key` TEXT PRIMARY KEY, `value` TEXT)");
|
||||
SQLite.Instance.Execute((values) =>
|
||||
SQLiteLegacy.Instance.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS `cookies` (`key` TEXT PRIMARY KEY, `value` TEXT)");
|
||||
SQLiteLegacy.Instance.Execute((values) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -92,7 +92,7 @@ namespace VRCX
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
new BinaryFormatter().Serialize(memoryStream, _cookieContainer);
|
||||
SQLite.Instance.ExecuteNonQuery(
|
||||
SQLiteLegacy.Instance.ExecuteNonQuery(
|
||||
"INSERT OR REPLACE INTO `cookies` (`key`, `value`) VALUES (@key, @value)",
|
||||
new Dictionary<string, object>() {
|
||||
{"@key", "default"},
|
||||
|
||||
423
WorldDBManager.cs
Normal file
423
WorldDBManager.cs
Normal file
@@ -0,0 +1,423 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class WorldDBManager
|
||||
{
|
||||
public static WorldDBManager Instance;
|
||||
private readonly HttpListener listener;
|
||||
private readonly WorldDatabase worldDB;
|
||||
|
||||
private string currentWorldId = null;
|
||||
private string lastError = null;
|
||||
|
||||
public WorldDBManager(string url)
|
||||
{
|
||||
Instance = this;
|
||||
// http://localhost:22500
|
||||
listener = new HttpListener();
|
||||
listener.Prefixes.Add(url);
|
||||
|
||||
worldDB = new WorldDatabase(Path.Combine(Program.AppDataDirectory, "VRCX-WorldData.db"));
|
||||
}
|
||||
|
||||
public async Task Start()
|
||||
{
|
||||
listener.Start();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var context = await listener.GetContextAsync();
|
||||
var request = context.Request;
|
||||
var responseData = new WorldDataRequestResponse(false, null, null);
|
||||
|
||||
if (MainForm.Instance?.Browser == null || MainForm.Instance.Browser.IsLoading || !MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
{
|
||||
responseData.Error = "VRCX not yet initialized. Try again in a moment.";
|
||||
responseData.StatusCode = 503;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
continue;
|
||||
};
|
||||
|
||||
switch (request.Url.LocalPath)
|
||||
{
|
||||
case "/vrcx/data/init":
|
||||
responseData = await HandleInitRequest(context);
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/data/get":
|
||||
responseData = await HandleDataRequest(context);
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/data/lasterror":
|
||||
responseData.OK = lastError == null;
|
||||
responseData.Data = lastError;
|
||||
lastError = null;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/data/getbulk":
|
||||
responseData = await HandleBulkDataRequest(context);
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/status":
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.Close();
|
||||
break;
|
||||
default:
|
||||
responseData.Error = "Invalid VRCX endpoint.";
|
||||
responseData.StatusCode = 404;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an HTTP listener request to initialize a connection to the world db manager.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP listener context object.</param>
|
||||
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
|
||||
private async Task<WorldDataRequestResponse> HandleInitRequest(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var responseData = new WorldDataRequestResponse(false, null, null);
|
||||
|
||||
if (request.QueryString["debug"] == "true")
|
||||
{
|
||||
if (!worldDB.DoesWorldExist("wrld_12345"))
|
||||
{
|
||||
worldDB.AddWorld("wrld_12345", "12345");
|
||||
worldDB.AddDataEntry("wrld_12345", "test", "testvalue");
|
||||
}
|
||||
|
||||
currentWorldId = "wrld_12345";
|
||||
responseData.OK = true;
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Data = "12345";
|
||||
return responseData;
|
||||
}
|
||||
|
||||
string worldId = await GetCurrentWorldID();
|
||||
|
||||
if (String.IsNullOrEmpty(worldId))
|
||||
{
|
||||
responseData.Error = "Failed to get/verify current world ID.";
|
||||
responseData.StatusCode = 500;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
currentWorldId = worldId;
|
||||
|
||||
var existsInDB = worldDB.DoesWorldExist(currentWorldId);
|
||||
string connectionKey;
|
||||
|
||||
if (!existsInDB)
|
||||
{
|
||||
connectionKey = GenerateWorldConnectionKey();
|
||||
worldDB.AddWorld(currentWorldId, connectionKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionKey = worldDB.GetWorldConnectionKey(currentWorldId);
|
||||
}
|
||||
|
||||
responseData.OK = true;
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Data = connectionKey;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an HTTP listener request for data from the world database.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP listener context object.</param>
|
||||
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
|
||||
private async Task<WorldDataRequestResponse> HandleDataRequest(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var responseData = new WorldDataRequestResponse(false, null, null);
|
||||
|
||||
var key = request.QueryString["key"];
|
||||
if (key == null)
|
||||
{
|
||||
responseData.Error = "Missing key parameter.";
|
||||
responseData.StatusCode = 400;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
var worldIdOverride = request.QueryString["world"];
|
||||
|
||||
if (worldIdOverride != null)
|
||||
{
|
||||
var world = worldDB.GetWorld(worldIdOverride);
|
||||
|
||||
if (world == null)
|
||||
{
|
||||
responseData.OK = false;
|
||||
responseData.Error = $"World ID '{worldIdOverride}' not initialized in this user's database.";
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Data = null;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
if (!world.AllowExternalRead)
|
||||
{
|
||||
responseData.OK = false;
|
||||
responseData.Error = $"World ID '{worldIdOverride}' does not allow external reads.";
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Data = null;
|
||||
return responseData;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWorldId == "wrld_12345" && worldIdOverride == null)
|
||||
worldIdOverride = "wrld_12345";
|
||||
|
||||
var worldId = worldIdOverride ?? await GetCurrentWorldID();
|
||||
|
||||
if (worldIdOverride == null && (String.IsNullOrEmpty(currentWorldId) || worldId != currentWorldId))
|
||||
{
|
||||
responseData.Error = "World ID not initialized.";
|
||||
responseData.StatusCode = 400;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
var value = worldDB.GetDataEntry(worldId, key);
|
||||
|
||||
responseData.OK = true;
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Error = null;
|
||||
responseData.Data = value?.Value;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an HTTP listener request for bulk data from the world database.
|
||||
/// </summary>
|
||||
/// <param name="context">The HTTP listener context object.</param>
|
||||
/// <returns>A <see cref="WorldDataRequestResponse"/> object containing the response data.</returns>
|
||||
private async Task<WorldDataRequestResponse> HandleBulkDataRequest(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var responseData = new WorldDataRequestResponse(false, null, null);
|
||||
|
||||
var keys = request.QueryString["keys"];
|
||||
if (keys == null)
|
||||
{
|
||||
responseData.Error = "Missing/invalid keys parameter.";
|
||||
responseData.StatusCode = 400;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
var keyArray = keys.Split(',');
|
||||
|
||||
var worldId = await GetCurrentWorldID();
|
||||
|
||||
if (String.IsNullOrEmpty(currentWorldId) || (worldId != currentWorldId && currentWorldId != "wrld_12345"))
|
||||
{
|
||||
responseData.Error = "World ID not initialized.";
|
||||
responseData.StatusCode = 400;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
var values = worldDB.GetDataEntries(currentWorldId, keyArray).ToList();
|
||||
|
||||
/*if (values == null)
|
||||
{
|
||||
responseData.Error = $"No data found for keys '{keys}' under world id '{currentWorldId}'.";
|
||||
responseData.StatusCode = 404;
|
||||
return responseData;
|
||||
}*/
|
||||
|
||||
// Build a dictionary of key/value pairs to send back. If a key doesn't exist in the database, the key will be included in the response as requested but with a null value.
|
||||
var data = new Dictionary<string, string>();
|
||||
for (int i = 0; i < keyArray.Length; i++)
|
||||
{
|
||||
string dataKey = keyArray[i];
|
||||
string dataValue = values?.Where(x => x.Key == dataKey).FirstOrDefault()?.Value; // Get the value from the list of data entries, if it exists, otherwise null
|
||||
|
||||
data.Add(dataKey, dataValue);
|
||||
}
|
||||
|
||||
responseData.OK = true;
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Error = null;
|
||||
responseData.Data = JsonConvert.SerializeObject(data);
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a unique identifier for a world connection request.
|
||||
/// </summary>
|
||||
/// <returns>A string representation of a GUID that can be used to identify the world on requests.</returns>
|
||||
private string GenerateWorldConnectionKey()
|
||||
{
|
||||
// Ditched the old method of generating a short key, since we're just going with json anyway who cares about a longer identifier
|
||||
// Since we can rely on this GUID being unique, we can use it to identify the world on requests instead of trying to keep track of the user's current world.
|
||||
// I uhh, should probably make sure this is actually unique though. Just in case. I'll do that later.
|
||||
return Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the current world by evaluating a JavaScript function in the main browser instance.
|
||||
/// </summary>
|
||||
/// <returns>The ID of the current world as a string, or null if it could not be retrieved.</returns>
|
||||
private async Task<string> GetCurrentWorldID()
|
||||
{
|
||||
JavascriptResponse funcResult = await MainForm.Instance.Browser.EvaluateScriptAsync("$app.API.actuallyGetCurrentLocation();", TimeSpan.FromSeconds(5));
|
||||
|
||||
try
|
||||
{
|
||||
funcResult = await MainForm.Instance.Browser.EvaluateScriptAsync("$app.API.actuallyGetCurrentLocation();", TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string worldId = funcResult?.Result?.ToString();
|
||||
|
||||
if (String.IsNullOrEmpty(worldId))
|
||||
{
|
||||
// implement
|
||||
// wait what was i going to do here again
|
||||
// seriously i forgot, hope it wasn't important
|
||||
return null;
|
||||
}
|
||||
|
||||
return worldId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a JSON response to an HTTP listener request with the specified response data and status code.
|
||||
/// </summary>
|
||||
/// <param name="response">The HTTP listener response object.</param>
|
||||
/// <param name="responseData">The response data to be serialized to JSON.</param>
|
||||
/// <param name="statusCode">The HTTP status code to be returned.</param>
|
||||
/// <returns>The HTTP listener response object.</returns>
|
||||
private HttpListenerResponse SendJsonResponse(HttpListenerResponse response, WorldDataRequestResponse responseData)
|
||||
{
|
||||
response.ContentType = "application/json";
|
||||
response.StatusCode = responseData.StatusCode;
|
||||
response.AddHeader("Cache-Control", "no-cache");
|
||||
|
||||
// Use newtonsoft.json to serialize WorldDataRequestResponse to json
|
||||
var json = JsonConvert.SerializeObject(responseData);
|
||||
var buffer = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
response.ContentLength64 = buffer.Length;
|
||||
response.OutputStream.Write(buffer, 0, buffer.Length);
|
||||
response.Close();
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Processes a JSON request containing world data and logs it to the world database.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON request containing the world data.</param>
|
||||
public async void ProcessLogWorldDataRequest(string json)
|
||||
{
|
||||
// Current format:
|
||||
// {
|
||||
// "requestType": "store",
|
||||
// "connectionKey": "abc123",
|
||||
// "key": "example_key",
|
||||
// "value": "example_value"
|
||||
// }
|
||||
|
||||
// * I could rate limit the processing of this, but I don't think it's necessary.
|
||||
// * At the amount of data you'd need to be spitting out to lag vrcx, you'd fill up the log file and lag out VRChat far before VRCX would have any issues; at least in my testing.
|
||||
// As long as malicious worlds can't permanently *store* stupid amounts of unculled data, this is pretty safe with the 10MB cap. If a world wants to just fill up a users HDD with logs, they can do that already anyway.
|
||||
|
||||
WorldDataRequest request;
|
||||
|
||||
try // try to deserialize the json into a WorldDataRequest object
|
||||
{
|
||||
request = JsonConvert.DeserializeObject<WorldDataRequest>(json);
|
||||
}
|
||||
catch (JsonReaderException ex)
|
||||
{
|
||||
this.lastError = ex.Message;
|
||||
// invalid json
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.lastError = ex.Message;
|
||||
// something else happened lol
|
||||
return;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(request.Key))
|
||||
{
|
||||
this.lastError = "`key` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(request.Value))
|
||||
{
|
||||
this.lastError = "`value` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(request.ConnectionKey))
|
||||
{
|
||||
this.lastError = "`connectionKey` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the connection key is a valid GUID. No point in doing anything else if it's not.
|
||||
if (!Guid.TryParse(request.ConnectionKey, out Guid _))
|
||||
{
|
||||
this.lastError = "Invalid GUID provided as connection key";
|
||||
// invalid guid
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the world ID from the connection key
|
||||
string worldId = worldDB.GetWorldByConnectionKey(request.ConnectionKey);
|
||||
if (worldId == null)
|
||||
{
|
||||
this.lastError = "Invalid connection key";
|
||||
// invalid connection key
|
||||
return;
|
||||
}
|
||||
|
||||
// Get/calculate the old and new data sizes for this key/the world
|
||||
int oldTotalDataSize = worldDB.GetWorldDataSize(worldId);
|
||||
int oldDataSize = worldDB.GetDataEntrySize(worldId, request.Key);
|
||||
int newDataSize = Encoding.UTF8.GetByteCount(request.Value);
|
||||
int newTotalDataSize = oldTotalDataSize + newDataSize - oldDataSize;
|
||||
|
||||
// Make sure we don't exceed 10MB total size for this world
|
||||
// This works, I tested it. Hopefully this prevents/limits any possible abuse.
|
||||
if (newTotalDataSize > 1024 * 1024 * 10)
|
||||
{
|
||||
this.lastError = $"You have hit the 10MB total data cap. The previous data entry was *not* stored. Your request was {newDataSize} bytes, your current shared byte total is {oldTotalDataSize} and you went over the table limit by {newTotalDataSize - (1024 * 1024 * 10)} bytes.";
|
||||
// too much data
|
||||
//throw new Exception("Too much data");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
worldDB.AddDataEntry(worldId, request.Key, request.Value, newDataSize);
|
||||
worldDB.UpdateWorldDataSize(worldId, newTotalDataSize);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
listener.Stop();
|
||||
listener.Close();
|
||||
worldDB.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
53
WorldDataRequestResponse.cs
Normal file
53
WorldDataRequestResponse.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class WorldDataRequestResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the request was successful.
|
||||
/// </summary>
|
||||
[JsonProperty("ok")]
|
||||
public bool OK { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the error message if the request was not successful.
|
||||
/// </summary>
|
||||
[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the data returned by the request.
|
||||
/// </summary>
|
||||
[JsonProperty("data")]
|
||||
public string Data { get; set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the response code.
|
||||
/// </summary>
|
||||
/// <value></value>
|
||||
[JsonProperty("statusCode")]
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
public WorldDataRequestResponse(bool ok, string error, string data)
|
||||
{
|
||||
OK = ok;
|
||||
Error = error;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
|
||||
public class WorldDataRequest
|
||||
{
|
||||
[JsonProperty("requestType")]
|
||||
public string RequestType;
|
||||
[JsonProperty("connectionKey")]
|
||||
public string ConnectionKey;
|
||||
[JsonProperty("key")]
|
||||
public string Key;
|
||||
[JsonProperty("value")]
|
||||
public string Value;
|
||||
}
|
||||
}
|
||||
270
WorldDatabase.cs
Normal file
270
WorldDatabase.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using SQLite;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
[Table("data")]
|
||||
public class WorldData
|
||||
{
|
||||
[PrimaryKey, AutoIncrement]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
[Column("world_id"), NotNull]
|
||||
public string WorldId { get; set; }
|
||||
[Column("key"), NotNull]
|
||||
public string Key { get; set; }
|
||||
[Column("value"), NotNull]
|
||||
public string Value { get; set; }
|
||||
[Column("value_size"), NotNull]
|
||||
public int ValueSize { get; set; }
|
||||
[Column("last_accessed")]
|
||||
public DateTimeOffset LastAccessed { get; set; }
|
||||
[Column("last_modified")]
|
||||
public DateTimeOffset LastModified { get; set; }
|
||||
}
|
||||
|
||||
[Table("worlds")]
|
||||
public class World
|
||||
{
|
||||
[PrimaryKey, AutoIncrement]
|
||||
[Column("id")]
|
||||
public int Id { get; set; }
|
||||
[Column("world_id"), NotNull]
|
||||
public string WorldId { get; set; }
|
||||
[Column("connection_key"), NotNull]
|
||||
public string ConnectionKey { get; set; }
|
||||
[Column("total_data_size"), NotNull]
|
||||
public int TotalDataSize { get; set; }
|
||||
[Column("allow_external_read")]
|
||||
public bool AllowExternalRead { get; set; }
|
||||
}
|
||||
|
||||
internal class WorldDatabase
|
||||
{
|
||||
private static SQLiteConnection sqlite;
|
||||
private readonly static string dbInitQuery = @"
|
||||
CREATE TABLE IF NOT EXISTS worlds (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
world_id TEXT NOT NULL UNIQUE,
|
||||
connection_key TEXT NOT NULL,
|
||||
total_data_size INTEGER DEFAULT 0,
|
||||
allow_external_read INTEGER DEFAULT 0
|
||||
);
|
||||
\
|
||||
CREATE TABLE IF NOT EXISTS data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
world_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
value_size INTEGER NOT NULL DEFAULT 0,
|
||||
last_accessed INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
last_modified INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (world_id) REFERENCES worlds(world_id) ON DELETE CASCADE,
|
||||
UNIQUE (world_id, key)
|
||||
);
|
||||
\
|
||||
CREATE TRIGGER IF NOT EXISTS data_update_trigger
|
||||
AFTER UPDATE ON data
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE data SET last_modified = (strftime('%s', 'now')) WHERE id = OLD.id;
|
||||
END;
|
||||
\
|
||||
CREATE TRIGGER IF NOT EXISTS data_insert_trigger
|
||||
AFTER INSERT ON data
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE data SET last_accessed = (strftime('%s', 'now')), last_modified = (strftime('%s', 'now')) WHERE id = NEW.id;
|
||||
END;";
|
||||
public WorldDatabase(string databaseLocation)
|
||||
{
|
||||
var options = new SQLiteConnectionString(databaseLocation, true);
|
||||
sqlite = new SQLiteConnection(options);
|
||||
sqlite.Execute(dbInitQuery);
|
||||
|
||||
// TODO: Split these init queries into their own functions so we can call/update them individually.
|
||||
var queries = dbInitQuery.Split('\\');
|
||||
sqlite.BeginTransaction();
|
||||
foreach (var query in queries)
|
||||
{
|
||||
sqlite.Execute(query);
|
||||
}
|
||||
sqlite.Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a world with the specified ID exists in the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to check for.</param>
|
||||
/// <returns>True if the world exists in the database, false otherwise.</returns>
|
||||
public bool DoesWorldExist(string worldId)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.WorldId);
|
||||
|
||||
return query.Any();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the ID of the world with the specified connection key from the database.
|
||||
/// </summary>
|
||||
/// <param name="connectionKey">The connection key of the world to get the ID for.</param>
|
||||
/// <returns>The ID of the world with the specified connection key, or null if no such world exists in the database.</returns>
|
||||
public string GetWorldByConnectionKey(string connectionKey)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.ConnectionKey == connectionKey).Select(w => w.WorldId);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection key for a world from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the connection key for.</param>
|
||||
/// <returns>The connection key for the specified world, or null if the world does not exist in the database.</returns>
|
||||
public string GetWorldConnectionKey(string worldId)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.ConnectionKey);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the connection key for a world in the database. If the world already exists in the database, the connection key is updated. Otherwise, a new world is added to the database with the specified connection key.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to set the connection key for.</param>
|
||||
/// <param name="connectionKey">The connection key to set for the world.</param>
|
||||
/// <returns>The connection key that was set.</returns>
|
||||
public string SetWorldConnectionKey(string worldId, string connectionKey)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.ConnectionKey);
|
||||
|
||||
if (query.Any())
|
||||
{
|
||||
sqlite.Execute("UPDATE worlds SET connection_key = ? WHERE world_id = ?", connectionKey, worldId);
|
||||
}
|
||||
else
|
||||
{
|
||||
sqlite.Insert(new World() { WorldId = worldId, ConnectionKey = connectionKey });
|
||||
}
|
||||
|
||||
return connectionKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new world to the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to add.</param>
|
||||
/// <param name="connectionKey">The connection key of the world to add.</param>
|
||||
/// <exception cref="SQLiteException">Thrown if a world with the specified ID already exists in the database.</exception>
|
||||
public void AddWorld(string worldId, string connectionKey)
|
||||
{
|
||||
// * This will throw an error if the world already exists.. so don't do that
|
||||
sqlite.Insert(new World() { WorldId = worldId, ConnectionKey = connectionKey });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the world with the specified ID from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get.</param>
|
||||
/// <returns>The world with the specified ID, or null if no such world exists in the database.</returns>
|
||||
public World GetWorld(string worldId)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId);
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total data size shared across all rows, in bytes, for the world with the specified ID from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the total data size for.</param>
|
||||
/// <returns>The total data size for the world, in bytes.</returns>
|
||||
public int GetWorldDataSize(string worldId)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.TotalDataSize);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the total data size, in bytes for the world with the specified ID in the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to update the total data size for.</param>
|
||||
/// <param name="size">The new total data size for the world, in bytes.</param>
|
||||
public void UpdateWorldDataSize(string worldId, int size)
|
||||
{
|
||||
sqlite.Execute("UPDATE worlds SET total_data_size = ? WHERE world_id = ?", size, worldId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds or updates a data entry in the database with the specified world ID, key, and value.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to add the data entry for.</param>
|
||||
/// <param name="key">The key of the data entry to add or replace.</param>
|
||||
/// <param name="value">The value of the data entry to add or replace.</param>
|
||||
/// <param name="dataSize">The size of the data entry to add or replace, in bytes. If null, the size is calculated from the value automatically.</param>
|
||||
public void AddDataEntry(string worldId, string key, string value, int? dataSize = null)
|
||||
{
|
||||
int byteSize = dataSize ?? Encoding.UTF8.GetByteCount(value);
|
||||
|
||||
// check if entry already exists;
|
||||
// INSERT OR REPLACE(InsertOrReplace method) deletes the old row and creates a new one, incrementing the id, which I don't want
|
||||
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && w.Key == key);
|
||||
if (query.Any())
|
||||
{
|
||||
sqlite.Execute("UPDATE data SET value = ?, value_size = ? WHERE world_id = ? AND key = ?", value, byteSize, worldId, key);
|
||||
}
|
||||
else
|
||||
{
|
||||
sqlite.Insert(new WorldData() { WorldId = worldId, Key = key, Value = value, ValueSize = byteSize });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data entry with the specified world ID and key from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the data entry for.</param>
|
||||
/// <param name="key">The key of the data entry to get.</param>
|
||||
/// <returns>The data entry with the specified world ID and key, or null if no such data entry exists in the database.</returns>
|
||||
public WorldData GetDataEntry(string worldId, string key)
|
||||
{
|
||||
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && w.Key == key);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data entries with the specified world ID and keys from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the data entries for.</param>
|
||||
/// <param name="keys">The keys of the data entries to get.</param>
|
||||
/// <returns>An enumerable collection of the data entries with the specified world ID and keys.</returns>
|
||||
public IEnumerable<WorldData> GetDataEntries(string worldId, string[] keys)
|
||||
{
|
||||
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && keys.Contains(w.Key));
|
||||
|
||||
return query.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the size of the data entry, in bytes, with the specified world ID and key from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the data entry size for.</param>
|
||||
/// <param name="key">The key of the data entry to get the size for.</param>
|
||||
/// <returns>The size of the data entry with the specified world ID and key, or 0 if no such data entry exists in the database.</returns>
|
||||
public int GetDataEntrySize(string worldId, string key)
|
||||
{
|
||||
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId && w.Key == key).Select(w => w.ValueSize);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
sqlite.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2092,6 +2092,47 @@ speechSynthesis.getVoices();
|
||||
});
|
||||
});
|
||||
|
||||
API.getUserApiCurrentLocation = function () {
|
||||
return this.currentUser?.presence?.world;
|
||||
};
|
||||
|
||||
// TODO: traveling to world checks
|
||||
API.actuallyGetCurrentLocation = async function () {
|
||||
const gameLogLocation = $app.lastLocation.location;
|
||||
console.log('gameLog Location', gameLogLocation);
|
||||
|
||||
const presence = this.currentUser.presence;
|
||||
let presenceLocation = this.currentUser.$locationTag;
|
||||
if (presenceLocation === 'traveling') {
|
||||
console.log("User is traveling, using $travelingToLocation", this.currentUser.$travelingToLocation);
|
||||
presenceLocation = this.currentUser.$travelingToLocation;
|
||||
}
|
||||
console.log('presence Location', presenceLocation);
|
||||
|
||||
// We want to use presence if it's valid to avoid extra API calls, but its prone to being outdated when this function is called.
|
||||
// So we check if the presence location is the same as the gameLog location; If it is, the presence is (probably) valid and we can use it.
|
||||
// If it's not, we need to get the user manually to get the correct location.
|
||||
// If the user happens to be offline or the api is just being dumb, we assume that the user logged into VRCX is different than the one in-game and return the gameLog location.
|
||||
// This is really dumb.
|
||||
if (presenceLocation === gameLogLocation) {
|
||||
console.log('ok presence return');
|
||||
return presence.world;
|
||||
}
|
||||
let args = await this.getUser({ userId: this.currentUser.id });
|
||||
let user = args.json
|
||||
|
||||
console.log('presence bad, got user', user);
|
||||
if (!$app.isRealInstance(user.location)) {
|
||||
console.warn(
|
||||
'presence invalid, user offline and/or instance invalid. returning gamelog location: ',
|
||||
gameLogLocation
|
||||
);
|
||||
return gameLogLocation;
|
||||
}
|
||||
console.warn('presence outdated, got user api location instead: ', user.location);
|
||||
return this.parseLocation(user.location).worldId;
|
||||
};
|
||||
|
||||
API.applyWorld = function (json) {
|
||||
var ref = this.cachedWorlds.get(json.id);
|
||||
if (typeof ref === 'undefined') {
|
||||
|
||||
BIN
lib/sqlite3.dll
Normal file
BIN
lib/sqlite3.dll
Normal file
Binary file not shown.
Reference in New Issue
Block a user