mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +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:
Vendored
+6
-1
@@ -3,5 +3,10 @@
|
|||||||
"i18n-ally.keystyle": "nested",
|
"i18n-ally.keystyle": "nested",
|
||||||
"i18n-ally.sourceLanguage": "en",
|
"i18n-ally.sourceLanguage": "en",
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"omnisharp.enableRoslynAnalyzers": true,
|
||||||
|
"omnisharp.useModernNet": false,
|
||||||
|
"[csharp]": {
|
||||||
|
"editor.defaultFormatter": "ms-dotnettools.csharp"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ namespace VRCX
|
|||||||
CheckGameRunning();
|
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)
|
public string MD5File(string Blob)
|
||||||
{
|
{
|
||||||
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
||||||
@@ -58,6 +63,11 @@ namespace VRCX
|
|||||||
return Convert.ToBase64String(md5);
|
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)
|
public string SignFile(string Blob)
|
||||||
{
|
{
|
||||||
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
||||||
@@ -68,12 +78,21 @@ namespace VRCX
|
|||||||
return Convert.ToBase64String(sigBytes);
|
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)
|
public string FileLength(string Blob)
|
||||||
{
|
{
|
||||||
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
||||||
return fileData.Length.ToString();
|
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()
|
public string ReadConfigFile()
|
||||||
{
|
{
|
||||||
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
|
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
|
||||||
@@ -87,6 +106,10 @@ namespace VRCX
|
|||||||
return json;
|
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)
|
public void WriteConfigFile(string json)
|
||||||
{
|
{
|
||||||
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
|
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
|
||||||
@@ -94,6 +117,11 @@ namespace VRCX
|
|||||||
File.WriteAllText(configFile, json);
|
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()
|
public string GetVRChatAppDataLocation()
|
||||||
{
|
{
|
||||||
var json = ReadConfigFile();
|
var json = ReadConfigFile();
|
||||||
@@ -114,21 +142,34 @@ namespace VRCX
|
|||||||
return cachePath;
|
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()
|
public string GetVRChatCacheLocation()
|
||||||
{
|
{
|
||||||
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
|
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows the developer tools for the main browser window.
|
||||||
|
/// </summary>
|
||||||
public void ShowDevTools()
|
public void ShowDevTools()
|
||||||
{
|
{
|
||||||
MainForm.Instance.Browser.ShowDevTools();
|
MainForm.Instance.Browser.ShowDevTools();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes all cookies from the global cef cookie manager.
|
||||||
|
/// </summary>
|
||||||
public void DeleteAllCookies()
|
public void DeleteAllCookies()
|
||||||
{
|
{
|
||||||
Cef.GetGlobalCookieManager().DeleteCookies();
|
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()
|
public void CheckGameRunning()
|
||||||
{
|
{
|
||||||
var isGameRunning = false;
|
var isGameRunning = false;
|
||||||
@@ -144,10 +185,16 @@ namespace VRCX
|
|||||||
isSteamVRRunning = true;
|
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)
|
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading)
|
||||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning);
|
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()
|
public int QuitGame()
|
||||||
{
|
{
|
||||||
var processes = Process.GetProcessesByName("vrchat");
|
var processes = Process.GetProcessesByName("vrchat");
|
||||||
@@ -157,6 +204,10 @@ namespace VRCX
|
|||||||
return processes.Length;
|
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)
|
public void StartGame(string arguments)
|
||||||
{
|
{
|
||||||
// try stream first
|
// 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)
|
public bool StartGameFromPath(string path, string arguments)
|
||||||
{
|
{
|
||||||
if (!path.EndsWith(".exe"))
|
if (!path.EndsWith(".exe"))
|
||||||
@@ -222,6 +279,11 @@ namespace VRCX
|
|||||||
return true;
|
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)
|
public void OpenLink(string url)
|
||||||
{
|
{
|
||||||
if (url.StartsWith("http://") ||
|
if (url.StartsWith("http://") ||
|
||||||
@@ -264,21 +326,43 @@ namespace VRCX
|
|||||||
VRCXVR.Instance.Restart();
|
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()
|
public string[][] GetVRDevices()
|
||||||
{
|
{
|
||||||
return VRCXVR.Instance.GetDevices();
|
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()
|
public float CpuUsage()
|
||||||
{
|
{
|
||||||
return CpuMonitor.Instance.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)
|
public string GetImage(string url, string fileId, string version)
|
||||||
{
|
{
|
||||||
return ImageCache.GetImage(url, fileId, 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 = "")
|
public void DesktopNotification(string BoldText, string Text = "", string Image = "")
|
||||||
{
|
{
|
||||||
var toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText02);
|
var toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText02);
|
||||||
@@ -297,6 +381,13 @@ namespace VRCX
|
|||||||
ToastNotificationManager.CreateToastNotifier("VRCX").Show(toast);
|
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 = "")
|
public void XSNotification(string Title, string Content, int Timeout, string Image = "")
|
||||||
{
|
{
|
||||||
bool UseBase64Icon;
|
bool UseBase64Icon;
|
||||||
@@ -332,6 +423,10 @@ namespace VRCX
|
|||||||
broadcastSocket.SendTo(byteBuffer, endPoint);
|
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)
|
public void DownloadVRCXUpdate(string url)
|
||||||
{
|
{
|
||||||
var Location = Path.Combine(Program.AppDataDirectory, "update.exe");
|
var Location = Path.Combine(Program.AppDataDirectory, "update.exe");
|
||||||
@@ -340,6 +435,9 @@ namespace VRCX
|
|||||||
client.DownloadFile(new Uri(url), Location);
|
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()
|
public void RestartApplication()
|
||||||
{
|
{
|
||||||
var VRCXProcess = new Process();
|
var VRCXProcess = new Process();
|
||||||
@@ -350,6 +448,10 @@ namespace VRCX
|
|||||||
Environment.Exit(0);
|
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()
|
public bool CheckForUpdateExe()
|
||||||
{
|
{
|
||||||
if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe")))
|
if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe")))
|
||||||
@@ -357,6 +459,9 @@ namespace VRCX
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends an IPC packet to announce the start of VRCX.
|
||||||
|
/// </summary>
|
||||||
public void IPCAnnounceStart()
|
public void IPCAnnounceStart()
|
||||||
{
|
{
|
||||||
IPCServer.Send(new IPCPacket
|
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)
|
public void SendIpc(string type, string data)
|
||||||
{
|
{
|
||||||
IPCServer.Send(new IPCPacket
|
IPCServer.Send(new IPCPacket
|
||||||
@@ -397,6 +507,10 @@ namespace VRCX
|
|||||||
VRCXVR._browser2.ExecuteScriptAsync($"$app.{function}", json);
|
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()
|
public string GetLaunchCommand()
|
||||||
{
|
{
|
||||||
var command = StartupArgs.LaunchCommand;
|
var command = StartupArgs.LaunchCommand;
|
||||||
@@ -404,11 +518,18 @@ namespace VRCX
|
|||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Focuses the main window of the VRCX application.
|
||||||
|
/// </summary>
|
||||||
public void FocusWindow()
|
public void FocusWindow()
|
||||||
{
|
{
|
||||||
MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); }));
|
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()
|
public string CustomCssPath()
|
||||||
{
|
{
|
||||||
var output = string.Empty;
|
var output = string.Empty;
|
||||||
@@ -418,6 +539,10 @@ namespace VRCX
|
|||||||
return output;
|
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()
|
public string CustomScriptPath()
|
||||||
{
|
{
|
||||||
var output = string.Empty;
|
var output = string.Empty;
|
||||||
@@ -442,6 +567,10 @@ namespace VRCX
|
|||||||
return Program.Version;
|
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()
|
public bool VrcClosedGracefully()
|
||||||
{
|
{
|
||||||
return LogWatcher.Instance.VrcClosedGracefully;
|
return LogWatcher.Instance.VrcClosedGracefully;
|
||||||
@@ -457,6 +586,10 @@ namespace VRCX
|
|||||||
WinformThemer.DoFunny();
|
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()
|
public double GetUptime()
|
||||||
{
|
{
|
||||||
using (var uptime = new PerformanceCounter("System", "System Up Time"))
|
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)
|
public int GetColourFromUserID(string userId)
|
||||||
{
|
{
|
||||||
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
|
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
|
||||||
return (hash[3] << 8) | hash[4];
|
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)
|
public Dictionary<string, int> GetColourBulk(List<object> userIds)
|
||||||
{
|
{
|
||||||
var output = new Dictionary<string, int>();
|
var output = new Dictionary<string, int>();
|
||||||
@@ -483,6 +627,10 @@ namespace VRCX
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the current text from the clipboard.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current text from the clipboard.</returns>
|
||||||
public string GetClipboard()
|
public string GetClipboard()
|
||||||
{
|
{
|
||||||
var clipboard = string.Empty;
|
var clipboard = string.Empty;
|
||||||
@@ -493,6 +641,11 @@ namespace VRCX
|
|||||||
return clipboard;
|
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)
|
public object GetVRChatRegistryKey(string key)
|
||||||
{
|
{
|
||||||
// https://answers.unity.com/questions/177945/playerprefs-changing-the-name-of-keys.html?childToView=208076#answer-208076
|
// 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;
|
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)
|
public bool SetVRChatRegistryKey(string key, string value)
|
||||||
{
|
{
|
||||||
uint hash = 5381;
|
uint hash = 5381;
|
||||||
@@ -562,6 +721,11 @@ namespace VRCX
|
|||||||
return true;
|
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)
|
public Dictionary<string, short> GetVRChatModerations(string currentUserId)
|
||||||
{
|
{
|
||||||
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
||||||
@@ -590,6 +754,12 @@ namespace VRCX
|
|||||||
return output;
|
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)
|
public short GetVRChatUserModeration(string currentUserId, string userId)
|
||||||
{
|
{
|
||||||
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
||||||
@@ -615,6 +785,13 @@ namespace VRCX
|
|||||||
return 0;
|
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)
|
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";
|
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
||||||
@@ -648,6 +825,10 @@ namespace VRCX
|
|||||||
return true;
|
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)
|
public void SetStartup(bool enabled)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -679,6 +860,13 @@ namespace VRCX
|
|||||||
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
|
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)
|
public void AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||||
@@ -696,7 +884,10 @@ namespace VRCX
|
|||||||
ScreenshotHelper.WritePNGDescription(path, metadataString);
|
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()
|
public void OpenScreenshotFileDialog()
|
||||||
{
|
{
|
||||||
if (dialogOpen) return;
|
if (dialogOpen) return;
|
||||||
@@ -737,6 +928,10 @@ namespace VRCX
|
|||||||
thread.Start();
|
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)
|
public void GetScreenshotMetadata(string path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(path))
|
if (string.IsNullOrEmpty(path))
|
||||||
@@ -818,6 +1013,9 @@ namespace VRCX
|
|||||||
ExecuteAppFunction("displayScreenshotMetadata", metadata.ToString(Formatting.Indented));
|
ExecuteAppFunction("displayScreenshotMetadata", metadata.ToString(Formatting.Indented));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the last screenshot taken by VRChat and retrieves its metadata.
|
||||||
|
/// </summary>
|
||||||
public void GetLastScreenshot()
|
public void GetLastScreenshot()
|
||||||
{
|
{
|
||||||
// Get the last screenshot taken by VRChat
|
// Get the last screenshot taken by VRChat
|
||||||
@@ -836,6 +1034,10 @@ namespace VRCX
|
|||||||
GetScreenshotMetadata(lastScreenshot);
|
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)
|
public void CopyImageToClipboard(string path)
|
||||||
{
|
{
|
||||||
// check if the file exists and is any image file type
|
// 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()
|
public void OpenShortcutFolder()
|
||||||
{
|
{
|
||||||
var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
|
var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
|
||||||
@@ -858,6 +1063,11 @@ namespace VRCX
|
|||||||
OpenFolderAndSelectItem(path, true);
|
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)
|
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.
|
// 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()
|
public void FlashWindow()
|
||||||
{
|
{
|
||||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); }));
|
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the user agent string for the browser.
|
||||||
|
/// </summary>
|
||||||
public void SetUserAgent()
|
public void SetUserAgent()
|
||||||
{
|
{
|
||||||
using (var client = MainForm.Instance.Browser.GetDevToolsClient())
|
using (var client = MainForm.Instance.Browser.GetDevToolsClient())
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ namespace VRCX
|
|||||||
return AppApi.Instance.GetVRChatCacheLocation();
|
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)
|
public string GetVRChatCacheFullLocation(string id, int version)
|
||||||
{
|
{
|
||||||
var cachePath = GetVRChatCacheLocation();
|
var cachePath = GetVRChatCacheLocation();
|
||||||
@@ -67,6 +73,12 @@ namespace VRCX
|
|||||||
return Path.Combine(cachePath, idHash, versionLocation);
|
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)
|
public long[] CheckVRChatCache(string id, int version)
|
||||||
{
|
{
|
||||||
long FileSize = -1;
|
long FileSize = -1;
|
||||||
@@ -155,6 +167,11 @@ namespace VRCX
|
|||||||
DownloadProgress = -16;
|
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)
|
public void DeleteCache(string id, int version)
|
||||||
{
|
{
|
||||||
var FullLocation = GetVRChatCacheFullLocation(id, version);
|
var FullLocation = GetVRChatCacheFullLocation(id, version);
|
||||||
@@ -162,6 +179,9 @@ namespace VRCX
|
|||||||
Directory.Delete(FullLocation, true);
|
Directory.Delete(FullLocation, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deletes the entire VRChat cache directory.
|
||||||
|
/// </summary>
|
||||||
public void DeleteAllCache()
|
public void DeleteAllCache()
|
||||||
{
|
{
|
||||||
var cachePath = GetVRChatCacheLocation();
|
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()
|
public void SweepCache()
|
||||||
{
|
{
|
||||||
var cachePath = GetVRChatCacheLocation();
|
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()
|
public long GetCacheSize()
|
||||||
{
|
{
|
||||||
var cachePath = GetVRChatCacheLocation();
|
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)
|
public long DirSize(DirectoryInfo d)
|
||||||
{
|
{
|
||||||
long size = 0;
|
long size = 0;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace VRCX
|
|||||||
|
|
||||||
private DateTime startTime = DateTime.Now;
|
private DateTime startTime = DateTime.Now;
|
||||||
private Dictionary<string, Process> startedProcesses = new Dictionary<string, Process>();
|
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;
|
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.
|
// 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.
|
// 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)
|
public static void KillProcessTree(int pid)
|
||||||
{
|
{
|
||||||
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||||
|
|||||||
+27
-2
@@ -14,6 +14,9 @@ using CefSharp;
|
|||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Monitors the VRChat log files for changes and provides access to the log data.
|
||||||
|
/// </summary>
|
||||||
public class LogWatcher
|
public class LogWatcher
|
||||||
{
|
{
|
||||||
public static readonly LogWatcher Instance;
|
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()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (m_ResetLog)
|
if (m_ResetLog)
|
||||||
@@ -157,6 +163,11 @@ namespace VRCX
|
|||||||
m_FirstRun = false;
|
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)
|
private void ParseLog(FileInfo fileInfo, LogContext logContext)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -224,6 +235,7 @@ namespace VRCX
|
|||||||
ParseLogUsharpVideoPlay(fileInfo, logContext, line, offset) ||
|
ParseLogUsharpVideoPlay(fileInfo, logContext, line, offset) ||
|
||||||
ParseLogUsharpVideoSync(fileInfo, logContext, line, offset) ||
|
ParseLogUsharpVideoSync(fileInfo, logContext, line, offset) ||
|
||||||
ParseLogWorldVRCX(fileInfo, logContext, line, offset) ||
|
ParseLogWorldVRCX(fileInfo, logContext, line, offset) ||
|
||||||
|
ParseLogWorldDataVRCX(fileInfo, logContext, line, offset) ||
|
||||||
ParseLogOnAudioConfigurationChanged(fileInfo, logContext, line, offset) ||
|
ParseLogOnAudioConfigurationChanged(fileInfo, logContext, line, offset) ||
|
||||||
ParseLogScreenshot(fileInfo, logContext, line, offset) ||
|
ParseLogScreenshot(fileInfo, logContext, line, offset) ||
|
||||||
ParseLogStringDownload(fileInfo, logContext, line, offset) ||
|
ParseLogStringDownload(fileInfo, logContext, line, offset) ||
|
||||||
@@ -593,6 +605,19 @@ namespace VRCX
|
|||||||
return true;
|
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)
|
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'
|
// 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)
|
private bool ParseOpenVRInit(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
||||||
{
|
{
|
||||||
// 2022.07.29 02:52:14 Log - OpenVR initialized!
|
// 2022.07.29 02:52:14 Log - OpenVR initialized!
|
||||||
|
|
||||||
// 2023.04.22 16:52:28 Log - Initializing VRSDK.
|
// 2023.04.22 16:52:28 Log - Initializing VRSDK.
|
||||||
// 2023.04.22 16:52:29 Log - StartVRSDK: Open VR Loader
|
// 2023.04.22 16:52:29 Log - StartVRSDK: Open VR Loader
|
||||||
|
|
||||||
@@ -944,7 +969,7 @@ namespace VRCX
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ParseDesktopMode(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
private bool ParseDesktopMode(FileInfo fileInfo, LogContext logContext, string line, int offset)
|
||||||
{
|
{
|
||||||
// 2023.04.22 16:54:18 Log - VR Disabled
|
// 2023.04.22 16:54:18 Log - VR Disabled
|
||||||
|
|||||||
+11
-2
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
@@ -78,8 +79,12 @@ namespace VRCX
|
|||||||
Application.EnableVisualStyles();
|
Application.EnableVisualStyles();
|
||||||
Application.SetCompatibleTextRenderingDefault(false);
|
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();
|
ProcessMonitor.Instance.Init();
|
||||||
SQLite.Instance.Init();
|
SQLiteLegacy.Instance.Init();
|
||||||
VRCXStorage.Load();
|
VRCXStorage.Load();
|
||||||
LoadFromConfig();
|
LoadFromConfig();
|
||||||
CpuMonitor.Instance.Init();
|
CpuMonitor.Instance.Init();
|
||||||
@@ -99,14 +104,18 @@ namespace VRCX
|
|||||||
AutoAppLaunchManager.Instance.Exit();
|
AutoAppLaunchManager.Instance.Exit();
|
||||||
LogWatcher.Instance.Exit();
|
LogWatcher.Instance.Exit();
|
||||||
WebApi.Instance.Exit();
|
WebApi.Instance.Exit();
|
||||||
|
worldDBServer.Stop();
|
||||||
|
|
||||||
Discord.Instance.Exit();
|
Discord.Instance.Exit();
|
||||||
CpuMonitor.Instance.Exit();
|
CpuMonitor.Instance.Exit();
|
||||||
VRCXStorage.Save();
|
VRCXStorage.Save();
|
||||||
SQLite.Instance.Exit();
|
SQLiteLegacy.Instance.Exit();
|
||||||
ProcessMonitor.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()
|
private static void LoadFromConfig()
|
||||||
{
|
{
|
||||||
if (!GPUFix)
|
if (!GPUFix)
|
||||||
|
|||||||
+151
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-1
@@ -181,7 +181,11 @@ namespace VRCX
|
|||||||
return new PNGChunk(type, chunkData);
|
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)
|
public static JObject ParseLfsPicture(string metadataString)
|
||||||
{
|
{
|
||||||
var metadata = new JObject();
|
var metadata = new JObject();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System;
|
||||||
using CefSharp;
|
using CefSharp;
|
||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
@@ -11,7 +12,7 @@ namespace VRCX
|
|||||||
repository.Register("SharedVariable", SharedVariable.Instance, false);
|
repository.Register("SharedVariable", SharedVariable.Instance, false);
|
||||||
repository.Register("WebApi", WebApi.Instance, true);
|
repository.Register("WebApi", WebApi.Instance, true);
|
||||||
repository.Register("VRCXStorage", VRCXStorage.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("LogWatcher", LogWatcher.Instance, true);
|
||||||
repository.Register("Discord", Discord.Instance, true);
|
repository.Register("Discord", Discord.Instance, true);
|
||||||
repository.Register("AssetBundleCacher", AssetBundleCacher.Instance, true);
|
repository.Register("AssetBundleCacher", AssetBundleCacher.Instance, true);
|
||||||
|
|||||||
+8
-3
@@ -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">
|
<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')" />
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
@@ -87,11 +87,13 @@
|
|||||||
<Compile Include="CefCustomDownloadHandler.cs" />
|
<Compile Include="CefCustomDownloadHandler.cs" />
|
||||||
<Compile Include="CefCustomDragHandler.cs" />
|
<Compile Include="CefCustomDragHandler.cs" />
|
||||||
<Compile Include="CefCustomMenuHandler.cs" />
|
<Compile Include="CefCustomMenuHandler.cs" />
|
||||||
|
<Compile Include="WorldDatabase.cs" />
|
||||||
<Compile Include="ImageCache.cs" />
|
<Compile Include="ImageCache.cs" />
|
||||||
<Compile Include="IPCClient.cs" />
|
<Compile Include="IPCClient.cs" />
|
||||||
<Compile Include="IPCServer.cs" />
|
<Compile Include="IPCServer.cs" />
|
||||||
<Compile Include="ProcessMonitor.cs" />
|
<Compile Include="ProcessMonitor.cs" />
|
||||||
<Compile Include="ScreenshotHelper.cs" />
|
<Compile Include="ScreenshotHelper.cs" />
|
||||||
|
<Compile Include="SQLite.cs" />
|
||||||
<Compile Include="StartupArgs.cs" />
|
<Compile Include="StartupArgs.cs" />
|
||||||
<Compile Include="Update.cs" />
|
<Compile Include="Update.cs" />
|
||||||
<Compile Include="CefService.cs" />
|
<Compile Include="CefService.cs" />
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
<Compile Include="NoopDragHandler.cs" />
|
<Compile Include="NoopDragHandler.cs" />
|
||||||
<Compile Include="OpenVR\openvr_api.cs" />
|
<Compile Include="OpenVR\openvr_api.cs" />
|
||||||
<Compile Include="WebApi.cs" />
|
<Compile Include="WebApi.cs" />
|
||||||
<Compile Include="SQLite.cs" />
|
<Compile Include="SQLiteLegacy.cs" />
|
||||||
<Compile Include="SharedVariable.cs" />
|
<Compile Include="SharedVariable.cs" />
|
||||||
<Compile Include="Util.cs" />
|
<Compile Include="Util.cs" />
|
||||||
<Compile Include="VRForm.cs">
|
<Compile Include="VRForm.cs">
|
||||||
@@ -123,6 +125,8 @@
|
|||||||
<Compile Include="AppApi.cs" />
|
<Compile Include="AppApi.cs" />
|
||||||
<Compile Include="VRCXStorage.cs" />
|
<Compile Include="VRCXStorage.cs" />
|
||||||
<Compile Include="JsonSerializer.cs" />
|
<Compile Include="JsonSerializer.cs" />
|
||||||
|
<Compile Include="WorldDataRequestResponse.cs" />
|
||||||
|
<Compile Include="WorldDBManager.cs" />
|
||||||
<Compile Include="WinApi.cs" />
|
<Compile Include="WinApi.cs" />
|
||||||
<Compile Include="WinformBase.cs">
|
<Compile Include="WinformBase.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
@@ -222,6 +226,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<PropertyGroup>
|
<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>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -60,8 +60,8 @@ namespace VRCX
|
|||||||
|
|
||||||
internal void LoadCookies()
|
internal void LoadCookies()
|
||||||
{
|
{
|
||||||
SQLite.Instance.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS `cookies` (`key` TEXT PRIMARY KEY, `value` TEXT)");
|
SQLiteLegacy.Instance.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS `cookies` (`key` TEXT PRIMARY KEY, `value` TEXT)");
|
||||||
SQLite.Instance.Execute((values) =>
|
SQLiteLegacy.Instance.Execute((values) =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -92,7 +92,7 @@ namespace VRCX
|
|||||||
using (var memoryStream = new MemoryStream())
|
using (var memoryStream = new MemoryStream())
|
||||||
{
|
{
|
||||||
new BinaryFormatter().Serialize(memoryStream, _cookieContainer);
|
new BinaryFormatter().Serialize(memoryStream, _cookieContainer);
|
||||||
SQLite.Instance.ExecuteNonQuery(
|
SQLiteLegacy.Instance.ExecuteNonQuery(
|
||||||
"INSERT OR REPLACE INTO `cookies` (`key`, `value`) VALUES (@key, @value)",
|
"INSERT OR REPLACE INTO `cookies` (`key`, `value`) VALUES (@key, @value)",
|
||||||
new Dictionary<string, object>() {
|
new Dictionary<string, object>() {
|
||||||
{"@key", "default"},
|
{"@key", "default"},
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) {
|
API.applyWorld = function (json) {
|
||||||
var ref = this.cachedWorlds.get(json.id);
|
var ref = this.cachedWorlds.get(json.id);
|
||||||
if (typeof ref === 'undefined') {
|
if (typeof ref === 'undefined') {
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user