mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
refactor: app.js (#1291)
* refactor: frontend * Fix avatar gallery sort * Update .NET dependencies * Update npm dependencies electron v37.1.0 * bulkRefreshFriends * fix dark theme * Remove crowdin * Fix config.json dialog not updating * VRCX log file fixes & add Cef log * Remove SharedVariable, fix startup * Revert init theme change * Logging date not working? Fix WinformThemer designer error * Add Cef request hander, no more escaping main page * clean * fix * fix * clean * uh * Apply thememode at startup, fixes random user colours * Split database into files * Instance info remove empty lines * Open external VRC links with VRCX * Electron fixes * fix userdialog style * ohhhh * fix store * fix store * fix: load all group members after kicking a user * fix: world dialog favorite button style * fix: Clear VRCX Cache Timer input value * clean * Fix VR overlay * Fix VR overlay 2 * Fix Discord discord rich presence for RPC worlds * Clean up age verified user tags * Fix playerList being occupied after program reload * no `this` * Fix login stuck loading * writable: false * Hide dialogs on logout * add flush sync option * rm LOGIN event * rm LOGOUT event * remove duplicate event listeners * remove duplicate event listeners * clean * remove duplicate event listeners * clean * fix theme style * fix t * clearable * clean * fix ipcEvent * Small changes * Popcorn Palace support * Remove checkActiveFriends * Clean up * Fix dragEnterCef * Block API requests when not logged in * Clear state on login & logout * Fix worldDialog instances not updating * use <script setup> * Fix avatar change event, CheckGameRunning at startup * Fix image dragging * fix * Remove PWI * fix updateLoop * add webpack-dev-server to dev environment * rm unnecessary chunks * use <script setup> * webpack-dev-server changes * use <script setup> * use <script setup> * Fix UGC text size * Split login event * t * use <script setup> * fix * Update .gitignore and enable checkJs in jsconfig * fix i18n t * use <script setup> * use <script setup> * clean * global types * fix * use checkJs for debugging * Add watchState for login watchers * fix .vue template * type fixes * rm Vue.filter * Cef v138.0.170, VC++ 2022 * Settings fixes * Remove 'USER:CURRENT' * clean up 2FA callbacks * remove userApply * rm i18n import * notification handling to use notification store methods * refactor favorite handling to use favorite store methods and clean up event emissions * refactor moderation handling to use dedicated functions for player moderation events * refactor friend handling to use dedicated functions for friend events * Fix program startup, move lang init * Fix friend state * Fix status change error * Fix user notes diff * fix * rm group event * rm auth event * rm avatar event * clean * clean * getUser * getFriends * getFavoriteWorlds, getFavoriteAvatars * AvatarGalleryUpload btn style & package.json update * Fix friend requests * Apply user * Apply world * Fix note diff * Fix VR overlay * Fixes * Update build scripts * Apply avatar * Apply instance * Apply group * update hidden VRC+ badge * Fix sameInstance "private" * fix 502/504 API errors * fix 502/504 API errors * clean * Fix friend in same instance on orange showing twice in friends list * Add back in broken friend state repair methods * add types --------- Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
@@ -19,18 +19,18 @@ namespace VRCX
|
||||
{
|
||||
// Create Instance before Cef tries to bind it
|
||||
}
|
||||
|
||||
|
||||
public void VrInit()
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.vrInit", "");
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
|
||||
}
|
||||
|
||||
|
||||
public void ToggleSystemMonitor(bool enabled)
|
||||
{
|
||||
SystemMonitor.Instance.Start(enabled);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current CPU usage as a percentage.
|
||||
/// </summary>
|
||||
@@ -39,7 +39,7 @@ namespace VRCX
|
||||
{
|
||||
return SystemMonitor.Instance.CpuUsage;
|
||||
}
|
||||
|
||||
|
||||
/// <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
|
||||
@@ -49,7 +49,7 @@ namespace VRCX
|
||||
{
|
||||
return Program.VRCXVRInstance.GetDevices();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the number of milliseconds that the system has been running.
|
||||
/// </summary>
|
||||
@@ -58,7 +58,7 @@ namespace VRCX
|
||||
{
|
||||
return SystemMonitor.Instance.UpTime;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the current language of the operating system.
|
||||
/// </summary>
|
||||
@@ -67,7 +67,7 @@ namespace VRCX
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
@@ -80,7 +80,7 @@ namespace VRCX
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
public bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace VRCX
|
||||
|
||||
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.game.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
|
||||
}
|
||||
|
||||
public override bool IsGameRunning()
|
||||
|
||||
@@ -24,9 +24,10 @@ namespace VRCX
|
||||
dragData.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vrcx.dragEnterCef", file);
|
||||
|
||||
// forgive me father for i have sinned once again
|
||||
Program.AppApiInstance.ExecuteAppFunction("dragEnterCef", file);
|
||||
dragData.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
74
Dotnet/Cef/CefCustomRequestHandler.cs
Normal file
74
Dotnet/Cef/CefCustomRequestHandler.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using CefSharp;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class CustomRequestHandler : IRequestHandler
|
||||
{
|
||||
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect)
|
||||
{
|
||||
if (Program.LaunchDebug ||
|
||||
request.Url.StartsWith("file://vrcx/") ||
|
||||
request.Url.StartsWith("chrome-extension://"))
|
||||
return false;
|
||||
|
||||
_logger.Error("Blocking navigation to: {Url}", request.Url);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void OnDocumentAvailableInMainFrame(IWebBrowser chromiumWebBrowser, IBrowser browser)
|
||||
{
|
||||
}
|
||||
|
||||
public bool OnOpenUrlFromTab(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, string targetUrl,
|
||||
WindowOpenDisposition targetDisposition, bool userGesture)
|
||||
{
|
||||
_logger.Debug("Blocking OnOpenUrlFromTab: {TargetUrl}",
|
||||
targetUrl);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame,
|
||||
IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool GetAuthCredentials(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, bool isProxy, string host,
|
||||
int port, string realm, string scheme, IAuthCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool OnCertificateError(IWebBrowser chromiumWebBrowser, IBrowser browser, CefErrorCode errorCode, string requestUrl,
|
||||
ISslInfo sslInfo, IRequestCallback callback)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OnSelectClientCertificate(IWebBrowser chromiumWebBrowser, IBrowser browser, bool isProxy, string host, int port,
|
||||
X509Certificate2Collection certificates, ISelectClientCertificateCallback callback)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public void OnRenderViewReady(IWebBrowser chromiumWebBrowser, IBrowser browser)
|
||||
{
|
||||
}
|
||||
|
||||
public void OnRenderProcessTerminated(IWebBrowser chromiumWebBrowser, IBrowser browser, CefTerminationStatus status,
|
||||
int errorCode, string errorMessage)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,8 @@ namespace VRCX
|
||||
{
|
||||
RootCachePath = userDataDir,
|
||||
CachePath = Path.Join(userDataDir, "cache"),
|
||||
LogSeverity = LogSeverity.Disable,
|
||||
LogSeverity = Program.LaunchDebug ? LogSeverity.Verbose : LogSeverity.Error,
|
||||
LogFile = Path.Join(Program.AppDataDirectory, "logs", "cef.log"),
|
||||
WindowlessRenderingEnabled = true,
|
||||
PersistSessionCookies = true,
|
||||
UserAgent = Program.Version,
|
||||
|
||||
@@ -8,7 +8,6 @@ namespace VRCX
|
||||
{
|
||||
repository.NameConverter = null;
|
||||
repository.Register("AppApi", Program.AppApiInstance);
|
||||
repository.Register("SharedVariable", SharedVariable.Instance);
|
||||
repository.Register("WebApi", WebApi.Instance);
|
||||
repository.Register("VRCXStorage", VRCXStorage.Instance);
|
||||
repository.Register("SQLite", SQLiteLegacy.Instance);
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Drawing;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
@@ -15,6 +15,7 @@ using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
|
||||
public partial class MainForm : WinformBase
|
||||
{
|
||||
public static MainForm Instance;
|
||||
@@ -50,30 +51,40 @@ namespace VRCX
|
||||
logger.Error(ex);
|
||||
}
|
||||
|
||||
Browser = new ChromiumWebBrowser("file://vrcx/index.html")
|
||||
Browser = new ChromiumWebBrowser(Program.LaunchDebug ? "http://localhost:9000/index.html" : "file://vrcx/index.html")
|
||||
{
|
||||
DragHandler = new CustomDragHandler(),
|
||||
MenuHandler = new CustomMenuHandler(),
|
||||
DownloadHandler = new CustomDownloadHandler(),
|
||||
RequestHandler = new CustomRequestHandler(),
|
||||
BrowserSettings =
|
||||
{
|
||||
DefaultEncoding = "UTF-8",
|
||||
},
|
||||
Dock = DockStyle.Fill
|
||||
};
|
||||
|
||||
Browser.IsBrowserInitializedChanged += (A, B) =>
|
||||
Browser.IsBrowserInitializedChanged += (_, _) =>
|
||||
{
|
||||
if (Program.LaunchDebug)
|
||||
Browser.ShowDevTools();
|
||||
};
|
||||
|
||||
JavascriptBindings.ApplyAppJavascriptBindings(Browser.JavascriptObjectRepository);
|
||||
Browser.ConsoleMessage += (_, args) =>
|
||||
Browser.AddressChanged += (_, addressChangedEventArgs) =>
|
||||
{
|
||||
logger.Debug(args.Message + " (" + args.Source + ":" + args.Line + ")");
|
||||
logger.Debug("Address changed: " + addressChangedEventArgs.Address);
|
||||
};
|
||||
Browser.LoadingStateChanged += (_, loadingFailedEventArgs) =>
|
||||
{
|
||||
if (loadingFailedEventArgs.IsLoading)
|
||||
logger.Debug("Loading page");
|
||||
else
|
||||
logger.Debug("Loaded page: " + loadingFailedEventArgs.Browser.MainFrame.Url);
|
||||
};
|
||||
Browser.ConsoleMessage += (_, consoleMessageEventArgs) =>
|
||||
{
|
||||
logger.Debug(consoleMessageEventArgs.Message + " (" + consoleMessageEventArgs.Source + ":" + consoleMessageEventArgs.Line + ")");
|
||||
};
|
||||
|
||||
JavascriptBindings.ApplyAppJavascriptBindings(Browser.JavascriptObjectRepository);
|
||||
Controls.Add(Browser);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace VRCX
|
||||
{
|
||||
protected override void OnHandleCreated(EventArgs e)
|
||||
{
|
||||
WinformThemer.SetThemeToGlobal(this);
|
||||
if (!DesignMode)
|
||||
WinformThemer.SetThemeToGlobal(this);
|
||||
base.OnHandleCreated(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using DiscordRPC;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class Discord
|
||||
{
|
||||
private readonly Logger _logger = LogManager.GetCurrentClassLogger();
|
||||
public static readonly Discord Instance;
|
||||
private readonly ReaderWriterLockSlim m_Lock;
|
||||
private readonly RichPresence m_Presence;
|
||||
@@ -54,8 +57,9 @@ namespace VRCX
|
||||
{
|
||||
Update();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error updating Discord Rich Presence {Error}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,7 +136,7 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAssets(string largeKey, string largeText, string smallKey, string smallText, string partyId, int partySize, int partyMax, string buttonText, string buttonUrl, string appId)
|
||||
public void SetAssets(string largeKey, string largeText, string smallKey, string smallText, string partyId, int partySize, int partyMax, string buttonText, string buttonUrl, string appId, int activityType = 0)
|
||||
{
|
||||
m_Lock.EnterWriteLock();
|
||||
try
|
||||
@@ -153,6 +157,7 @@ namespace VRCX
|
||||
m_Presence.Party.ID = partyId;
|
||||
m_Presence.Party.Size = partySize;
|
||||
m_Presence.Party.Max = partyMax;
|
||||
m_Presence.Type = (ActivityType)activityType;
|
||||
Button[] buttons = [];
|
||||
if (!string.IsNullOrEmpty(buttonUrl))
|
||||
{
|
||||
@@ -207,6 +212,10 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error setting timestamps in Discord Rich Presence {Error}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_Lock.ExitWriteLock();
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace VRCX
|
||||
|
||||
#if !LINUX
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.ipcEvent", packet);
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vrcx.ipcEvent", packet);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ namespace VRCX
|
||||
m_LogQueue.Enqueue(logLine);
|
||||
#else
|
||||
if (MainForm.Instance != null && MainForm.Instance.Browser != null)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine);
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.gameLog.addGameLogEvent", logLine);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -729,10 +729,9 @@ namespace VRCX
|
||||
return false;
|
||||
|
||||
var data = line.Substring(offset + 13);
|
||||
|
||||
#if !LINUX
|
||||
WorldDBManager.Instance.ProcessLogWorldDataRequest(data);
|
||||
#endif
|
||||
|
||||
// PWI, deprecated
|
||||
logger.Info("VRCX-World data: {0}", data);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -188,13 +188,13 @@ namespace VRCX
|
||||
var dashboardHandle = 0UL;
|
||||
|
||||
_wristOverlay = new OffScreenBrowser(
|
||||
"file://vrcx/vr.html?1",
|
||||
Program.LaunchDebug ? "http://localhost:9000/vr.html?1": "file://vrcx/vr.html?1",
|
||||
512,
|
||||
512
|
||||
);
|
||||
|
||||
_hmdOverlay = new OffScreenBrowser(
|
||||
"file://vrcx/vr.html?2",
|
||||
Program.LaunchDebug ? "http://localhost:9000/vr.html?2": "file://vrcx/vr.html?2",
|
||||
1024,
|
||||
1024
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace VRCX
|
||||
)
|
||||
{
|
||||
DragHandler = new CefNoopDragHandler(),
|
||||
RequestHandler = new CustomRequestHandler(),
|
||||
BrowserSettings =
|
||||
{
|
||||
DefaultEncoding = "UTF-8",
|
||||
|
||||
@@ -1,750 +0,0 @@
|
||||
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;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class WorldDBManager
|
||||
{
|
||||
public static readonly WorldDBManager Instance;
|
||||
private readonly HttpListener listener;
|
||||
private readonly WorldDatabase worldDB;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private const string WorldDBServerUrl = "http://127.0.0.1:22500/";
|
||||
|
||||
private string lastError = null;
|
||||
private bool debugWorld = false;
|
||||
|
||||
static WorldDBManager()
|
||||
{
|
||||
Instance = new WorldDBManager();
|
||||
}
|
||||
|
||||
public WorldDBManager()
|
||||
{
|
||||
// http://localhost:22500
|
||||
listener = new HttpListener();
|
||||
listener.Prefixes.Add(WorldDBServerUrl);
|
||||
|
||||
worldDB = new WorldDatabase(Path.Join(Program.AppDataDirectory, "VRCX-WorldData.db"));
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
if (VRCXStorage.Instance.Get("VRCX_DisableWorldDatabase") == "true")
|
||||
{
|
||||
logger.Info("World database is disabled. Not starting.");
|
||||
return;
|
||||
}
|
||||
|
||||
Task.Run(Start);
|
||||
}
|
||||
|
||||
private async Task Start()
|
||||
{
|
||||
// typing this in vr gonna kms
|
||||
try
|
||||
{
|
||||
listener.Start();
|
||||
}
|
||||
catch (HttpListenerException e)
|
||||
{
|
||||
logger.Error(e, "Failed to start HTTP listener. Is VRCX already running?");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.Info("Listening for requests on {0}", listener.Prefixes.First());
|
||||
while (true)
|
||||
{
|
||||
var context = await listener.GetContextAsync();
|
||||
var request = context.Request;
|
||||
var responseData = new WorldDataRequestResponse(false, null, null);
|
||||
|
||||
try
|
||||
{
|
||||
if (MainForm.Instance?.Browser == null || MainForm.Instance.Browser.IsLoading || !MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
{
|
||||
logger.Error("Received a request to {0} while VRCX is still initializing the browser window. Responding with error 503.", request.Url);
|
||||
|
||||
responseData.Error = "VRCX not yet initialized. Try again in a moment.";
|
||||
responseData.StatusCode = 503;
|
||||
responseData.ConnectionKey = null;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
continue;
|
||||
};
|
||||
|
||||
logger.Debug("Received a request to '{0}'", request.Url);
|
||||
|
||||
// TODO: Maybe an endpoint for getting a group of arbitrary keys by a group 'name'? eg; /getgroup?name=testgroup1 would return all keys with the column group set to 'testgroup1'
|
||||
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/getall":
|
||||
responseData = await HandleAllDataRequest(context);
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/data/lasterror":
|
||||
responseData.OK = lastError == null;
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Data = lastError;
|
||||
responseData.ConnectionKey = null;
|
||||
lastError = null;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/data/getbulk":
|
||||
responseData = await HandleBulkDataRequest(context);
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/data/settings":
|
||||
responseData = await HandleSetSettingsRequest(context);
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
case "/vrcx/status":
|
||||
// Send a blank 200 response to indicate that the server is running.
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.Close();
|
||||
break;
|
||||
default:
|
||||
responseData.Error = "Invalid VRCX endpoint.";
|
||||
responseData.StatusCode = 404;
|
||||
responseData.ConnectionKey = null;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
break;
|
||||
}
|
||||
|
||||
if (context.Response.StatusCode != 200)
|
||||
{
|
||||
logger.Warn("Received a request to '{0}' that returned a non-successful response. Error: {1} - {2}", request.Url, responseData.StatusCode, responseData.Error);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, $"Exception while processing a request to endpoint '{request.Url}'.");
|
||||
|
||||
responseData.Error = $"VRCX has encountered an exception while processing the url '{request.Url}': {ex.Message}";
|
||||
responseData.StatusCode = 500;
|
||||
responseData.ConnectionKey = null;
|
||||
SendJsonResponse(context.Response, responseData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private async Task<WorldDataRequestResponse> HandleSetSettingsRequest(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
|
||||
string worldId = await GetCurrentWorldID();
|
||||
string set = request.QueryString["set"];
|
||||
string value = request.QueryString["value"];
|
||||
|
||||
if (!TryInitializeWorld(worldId, out string connectionKey))
|
||||
{
|
||||
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
|
||||
}
|
||||
|
||||
if (set != null && value != null)
|
||||
{
|
||||
switch (set)
|
||||
{
|
||||
case "externalReads":
|
||||
if (request.QueryString["value"] == "true")
|
||||
{
|
||||
worldDB.SetWorldAllowExternalRead(worldId, true);
|
||||
}
|
||||
else if (request.QueryString["value"] == "false")
|
||||
{
|
||||
worldDB.SetWorldAllowExternalRead(worldId, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ConstructErrorResponse(400, "Invalid value for 'externalReads' setting.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return ConstructErrorResponse(400, "Invalid setting name.");
|
||||
}
|
||||
}
|
||||
|
||||
return ConstructSuccessResponse(null, connectionKey);
|
||||
}
|
||||
|
||||
|
||||
/// <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;
|
||||
|
||||
if (request.QueryString["debug"] == "true")
|
||||
{
|
||||
debugWorld = true;
|
||||
}
|
||||
else if (request.QueryString["debug"] == "false")
|
||||
{
|
||||
debugWorld = false;
|
||||
}
|
||||
|
||||
string worldId = await GetCurrentWorldID();
|
||||
|
||||
if (TryInitializeWorld(worldId, out string connectionKey))
|
||||
{
|
||||
logger.Info("Initialized a connection to the world database for world ID '{0}' with connection key {1}.", worldId, connectionKey);
|
||||
return ConstructSuccessResponse(connectionKey, connectionKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 key = request.QueryString["key"];
|
||||
if (key == null)
|
||||
{
|
||||
return ConstructErrorResponse(400, "Missing key parameter.");
|
||||
}
|
||||
|
||||
var worldId = await GetCurrentWorldID();
|
||||
|
||||
if (!TryInitializeWorld(worldId, out string connectionKey))
|
||||
{
|
||||
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
|
||||
}
|
||||
|
||||
var worldOverride = request.QueryString["world"];
|
||||
if (worldOverride == "global")
|
||||
{
|
||||
TryInitializeWorld(worldOverride, out connectionKey);
|
||||
}
|
||||
if (worldOverride != null && worldId != worldOverride)
|
||||
{
|
||||
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride) || worldOverride == "global";
|
||||
if (!allowed)
|
||||
{
|
||||
return ConstructSuccessResponse(null, connectionKey);
|
||||
}
|
||||
|
||||
var otherValue = worldDB.GetDataEntry(worldOverride, key);
|
||||
|
||||
logger.Debug("Serving a request for data with key '{0}' from world ID '{1}' requested by world ID '{2}' with connection key {3}.", key, worldOverride, worldId, connectionKey);
|
||||
|
||||
// This value is intended to be null if the key doesn't exist.
|
||||
return ConstructSuccessResponse(otherValue?.Value, connectionKey);
|
||||
}
|
||||
|
||||
var value = worldDB.GetDataEntry(worldId, key);
|
||||
|
||||
logger.Debug("Serving a request for data with key '{0}' from world ID '{1}' with connection key {2}.", key, worldId, connectionKey);
|
||||
// This value is intended to be null if the key doesn't exist.
|
||||
return ConstructSuccessResponse(value?.Value, connectionKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles an HTTP listener request for all data from the world database for a given world.
|
||||
/// </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> HandleAllDataRequest(HttpListenerContext context)
|
||||
{
|
||||
var request = context.Request;
|
||||
var worldId = await GetCurrentWorldID();
|
||||
|
||||
if (!TryInitializeWorld(worldId, out string connectionKey))
|
||||
{
|
||||
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
|
||||
}
|
||||
|
||||
var worldOverride = request.QueryString["world"];
|
||||
if (worldOverride == "global")
|
||||
{
|
||||
TryInitializeWorld(worldOverride, out connectionKey);
|
||||
}
|
||||
if (worldOverride != null && worldId != worldOverride)
|
||||
{
|
||||
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride) || worldOverride == "global";
|
||||
if (!allowed)
|
||||
{
|
||||
return ConstructSuccessResponse(null, connectionKey);
|
||||
}
|
||||
|
||||
var otherEntries = worldDB.GetAllDataEntries(worldOverride);
|
||||
|
||||
var otherData = new Dictionary<string, string>();
|
||||
foreach (var entry in otherEntries)
|
||||
{
|
||||
otherData.Add(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' requested by {2} with connection key {3}.", otherData.Count, worldOverride, worldId, connectionKey);
|
||||
return ConstructSuccessResponse(JsonConvert.SerializeObject(otherData), connectionKey);
|
||||
}
|
||||
|
||||
var entries = worldDB.GetAllDataEntries(worldId);
|
||||
|
||||
var data = new Dictionary<string, string>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
data.Add(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' with connection key {2}.", data.Count, worldId, connectionKey);
|
||||
return ConstructSuccessResponse(JsonConvert.SerializeObject(data), connectionKey);
|
||||
}
|
||||
|
||||
/// <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 keys = request.QueryString["keys"];
|
||||
if (keys == null)
|
||||
{
|
||||
return ConstructErrorResponse(400, "Missing/invalid keys parameter.");
|
||||
}
|
||||
|
||||
var keyArray = keys.Split(',');
|
||||
|
||||
var worldId = await GetCurrentWorldID();
|
||||
|
||||
if (!TryInitializeWorld(worldId, out string connectionKey))
|
||||
{
|
||||
return ConstructErrorResponse(500, "Failed to get/verify current world ID.");
|
||||
}
|
||||
|
||||
var worldOverride = request.QueryString["world"];
|
||||
if (worldOverride == "global")
|
||||
{
|
||||
TryInitializeWorld(worldOverride, out connectionKey);
|
||||
}
|
||||
if (worldOverride != null && worldId != worldOverride)
|
||||
{
|
||||
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride) || worldOverride == "global";
|
||||
if (!allowed)
|
||||
{
|
||||
return ConstructSuccessResponse(null, connectionKey);
|
||||
}
|
||||
|
||||
var otherEntries = worldDB.GetAllDataEntries(worldOverride);
|
||||
|
||||
var otherData = new Dictionary<string, string>();
|
||||
foreach (var entry in otherEntries)
|
||||
{
|
||||
otherData.Add(entry.Key, entry.Value);
|
||||
}
|
||||
|
||||
logger.Debug("Serving a request for all data ({0} entries) for world ID '{1}' requested by {2} with connection key {3}.", otherData.Count, worldOverride, worldId, connectionKey);
|
||||
return ConstructSuccessResponse(JsonConvert.SerializeObject(otherData), connectionKey);
|
||||
}
|
||||
|
||||
var values = worldDB.GetDataEntries(worldId, keyArray).ToList();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
logger.Debug("Serving a request for bulk data with keys '{0}' from world ID '{1}' with connection key {2}.", keys, worldId, connectionKey);
|
||||
return ConstructSuccessResponse(JsonConvert.SerializeObject(data), connectionKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to initialize a world with the given ID by generating a connection key and adding it to the world database if it does not already exist.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to initialize.</param>
|
||||
/// <param name="connectionKey">The connection key generated for the world.</param>
|
||||
/// <returns>True if the world was successfully initialized, false otherwise.</returns>
|
||||
private bool TryInitializeWorld(string worldId, out string connectionKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(worldId))
|
||||
{
|
||||
connectionKey = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var existsInDB = worldDB.DoesWorldExist(worldId);
|
||||
|
||||
if (!existsInDB)
|
||||
{
|
||||
connectionKey = GenerateWorldConnectionKey();
|
||||
worldDB.AddWorld(worldId, connectionKey);
|
||||
logger.Info("Added new world ID '{0}' with connection key '{1}' to the database.", worldId, connectionKey);
|
||||
}
|
||||
else
|
||||
{
|
||||
connectionKey = worldDB.GetWorldConnectionKey(worldId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
if (debugWorld) return "12345";
|
||||
|
||||
// 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()
|
||||
{
|
||||
if (debugWorld) return "wrld_12345";
|
||||
|
||||
JavascriptResponse funcResult;
|
||||
|
||||
try
|
||||
{
|
||||
funcResult = await MainForm.Instance.Browser.EvaluateScriptAsync("$app.API.actuallyGetCurrentLocation();", TimeSpan.FromSeconds(5));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to evaluate actuallyGetCurrentLocation JS function to get current world ID.");
|
||||
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
|
||||
logger.Warn("actuallyGetCurrentLocation returned null or empty.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return worldId;
|
||||
}
|
||||
|
||||
private WorldDataRequestResponse ConstructSuccessResponse(string data = null, string connectionKey = null)
|
||||
{
|
||||
var responseData = new WorldDataRequestResponse(true, null, null);
|
||||
|
||||
responseData.StatusCode = 200;
|
||||
responseData.Error = null;
|
||||
responseData.OK = true;
|
||||
responseData.Data = data;
|
||||
responseData.ConnectionKey = connectionKey;
|
||||
return responseData;
|
||||
}
|
||||
|
||||
private WorldDataRequestResponse ConstructErrorResponse(int statusCode, string error)
|
||||
{
|
||||
var responseData = new WorldDataRequestResponse(true, null, null);
|
||||
|
||||
responseData.StatusCode = statusCode;
|
||||
responseData.Error = error;
|
||||
responseData.OK = false;
|
||||
responseData.Data = null;
|
||||
responseData.ConnectionKey = null;
|
||||
|
||||
return responseData;
|
||||
}
|
||||
|
||||
/// <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 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)
|
||||
{
|
||||
logger.Error(ex, json.ToString());
|
||||
this.lastError = ex.Message;
|
||||
// invalid json
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, json.ToString());
|
||||
this.lastError = ex.Message;
|
||||
// something else happened lol
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(request.RequestType))
|
||||
{
|
||||
logger.Warn("World tried to store data with no request type provided. Request: ", json);
|
||||
this.lastError = "`requestType` 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 (!debugWorld && !Guid.TryParse(request.ConnectionKey, out Guid _))
|
||||
{
|
||||
logger.Warn("World tried to store data with an invalid GUID as a connection key '{0}'", request.ConnectionKey);
|
||||
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);
|
||||
|
||||
// Global override
|
||||
if (request.ConnectionKey == "global")
|
||||
{
|
||||
worldId = "global";
|
||||
}
|
||||
|
||||
// World ID is null, which means the connection key is invalid (or someone just deleted a world from the DB while VRCX was running lol).
|
||||
if (worldId == null)
|
||||
{
|
||||
logger.Warn("World tried to store data under {0} with an invalid connection key {1}", request.Key, request.ConnectionKey);
|
||||
this.lastError = "Invalid connection key";
|
||||
// invalid connection key
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string requestType = request.RequestType.ToLower();
|
||||
switch (requestType)
|
||||
{
|
||||
case "store":
|
||||
if (String.IsNullOrEmpty(request.Key))
|
||||
{
|
||||
logger.Warn("World {0} tried to store data with no key provided", worldId);
|
||||
this.lastError = "`key` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.Key.Length > 255)
|
||||
{
|
||||
logger.Warn("World {0} tried to store data with a key that was too long ({1}/256 characters)", worldId, request.Key.Length);
|
||||
this.lastError = "`key` is too long. Keep it below <256 characters.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.Value == null)
|
||||
{
|
||||
logger.Warn("World {0} tried to store data under key {1} with null value", worldId, request.Key);
|
||||
this.lastError = "`value` is null";
|
||||
return;
|
||||
}
|
||||
|
||||
StoreWorldData(worldId, request.Key, request.Value);
|
||||
|
||||
break;
|
||||
case "delete":
|
||||
if (String.IsNullOrEmpty(request.Key))
|
||||
{
|
||||
logger.Warn("World {0} tried to delete data with no key provided", worldId);
|
||||
this.lastError = "`key` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
DeleteWorldData(worldId, request.Key);
|
||||
break;
|
||||
case "delete-all":
|
||||
|
||||
logger.Info("World {0} requested to delete all data.", worldId);
|
||||
|
||||
|
||||
worldDB.DeleteAllDataEntriesForWorld(worldId);
|
||||
worldDB.UpdateWorldDataSize(worldId, 0);
|
||||
break;
|
||||
case "set-setting":
|
||||
if (String.IsNullOrEmpty(request.Key))
|
||||
{
|
||||
logger.Warn("World {0} tried to delete data with no key provided", worldId);
|
||||
this.lastError = "`key` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
if (String.IsNullOrEmpty(request.Value))
|
||||
{
|
||||
logger.Warn("World {0} tried to set settings with no value provided", worldId);
|
||||
this.lastError = "`value` is missing or null";
|
||||
return;
|
||||
}
|
||||
|
||||
SetWorldProperty(worldId, request.Key, request.Value);
|
||||
break;
|
||||
default:
|
||||
logger.Warn("World {0} sent an invalid request type '{0}'", worldId, request.RequestType);
|
||||
this.lastError = "Invalid request type";
|
||||
// invalid request type
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to process world data request for world {0}", worldId);
|
||||
logger.Error("Failed Request: {0}", json);
|
||||
this.lastError = ex.Message;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property for a given world in the world database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to set the property for.</param>
|
||||
/// <param name="key">The key of the property to set.</param>
|
||||
/// <param name="value">The value to set the property to.</param>
|
||||
public void SetWorldProperty(string worldId, string key, string value)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case "externalReads":
|
||||
if (bool.TryParse(value, out bool result))
|
||||
{
|
||||
worldDB.SetWorldAllowExternalRead(worldId, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("World {0} tried to set externalReads to an invalid value '{1}'", worldId, value);
|
||||
this.lastError = "Invalid value for externalReads";
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a data entry for a given world in the world database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to store the data entry for.</param>
|
||||
/// <param name="key">The key of the data entry to store.</param>
|
||||
/// <param name="value">The value of the data entry to store.</param>
|
||||
public void StoreWorldData(string worldId, string key, string value)
|
||||
{
|
||||
// Get/calculate the old and new data sizes for this key/the world
|
||||
int oldTotalDataSize = worldDB.GetWorldDataSize(worldId);
|
||||
int oldDataSize = worldDB.GetDataEntrySize(worldId, key);
|
||||
int newDataSize = Encoding.UTF8.GetByteCount(value);
|
||||
int newTotalDataSize = oldTotalDataSize + newDataSize - oldDataSize;
|
||||
|
||||
// Make sure we don't exceed 10MB total size for a world
|
||||
// This works, I tested it. Hopefully this prevents/limits any possible abuse.
|
||||
if (newTotalDataSize > 1024 * 1024 * 10)
|
||||
{
|
||||
logger.Warn("World {0} exceeded 10MB total data size trying to store key {0}. {1}:{2} + {3} = {4}", key, worldId, oldTotalDataSize - oldDataSize, newDataSize, newTotalDataSize);
|
||||
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.";
|
||||
return;
|
||||
}
|
||||
|
||||
worldDB.AddDataEntry(worldId, key, value, newDataSize);
|
||||
worldDB.UpdateWorldDataSize(worldId, newTotalDataSize);
|
||||
logger.Info("World {0} stored data entry {1} with size {2} bytes", worldId, key, newDataSize);
|
||||
logger.Debug("{0} : {1}", key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a data entry for a given world from the world database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to delete the data entry from.</param>
|
||||
/// <param name="key">The key of the data entry to delete.</param>
|
||||
public void DeleteWorldData(string worldId, string key)
|
||||
{
|
||||
int oldTotalDataSize = worldDB.GetWorldDataSize(worldId);
|
||||
int oldDataSize = worldDB.GetDataEntrySize(worldId, key);
|
||||
int newTotalDataSize = oldTotalDataSize - oldDataSize;
|
||||
|
||||
worldDB.DeleteDataEntry(worldId, key);
|
||||
worldDB.UpdateWorldDataSize(worldId, newTotalDataSize);
|
||||
logger.Info("World {0} deleted data entry {1} with size {2} bytes.", worldId, key, oldDataSize);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
worldDB?.Close();
|
||||
listener?.Stop();
|
||||
listener?.Close();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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; }
|
||||
[JsonProperty("connectionKey")]
|
||||
public string ConnectionKey { 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;
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
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>
|
||||
/// Sets the value of the allow_external_read field for the world with the specified ID in the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to set the allow_external_read field for.</param>
|
||||
/// <param name="allowExternalRead">The value to set for the allow_external_read field.</param>
|
||||
public void SetWorldAllowExternalRead(string worldId, bool allowExternalRead)
|
||||
{
|
||||
sqlite.Execute("UPDATE worlds SET allow_external_read = ? WHERE world_id = ?", allowExternalRead, worldId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the allow_external_read field for the world with the specified ID from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the allow_external_read field for.</param>
|
||||
/// <returns>The value of the allow_external_read field for the specified world.</returns>
|
||||
public bool GetWorldAllowExternalRead(string worldId)
|
||||
{
|
||||
var query = sqlite.Table<World>().Where(w => w.WorldId == worldId).Select(w => w.AllowExternalRead);
|
||||
|
||||
return query.FirstOrDefault();
|
||||
}
|
||||
|
||||
/// <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 all data entries for the world with the specified ID from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to get the data entries for.</param>
|
||||
/// <returns>An enumerable collection of all data entries for the world with the specified ID.</returns>
|
||||
public IEnumerable<WorldData> GetAllDataEntries(string worldId)
|
||||
{
|
||||
var query = sqlite.Table<WorldData>().Where(w => w.WorldId == worldId).Take(10000);
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the data entry with the specified world ID and key from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to delete the data entry from.</param>
|
||||
/// <param name="key">The key of the data entry to delete.</param>
|
||||
public void DeleteDataEntry(string worldId, string key)
|
||||
{
|
||||
sqlite.Execute("DELETE FROM data WHERE world_id = ? AND key = ?", worldId, key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all data entries for the world with the specified ID from the database.
|
||||
/// </summary>
|
||||
/// <param name="worldId">The ID of the world to delete all data entries for.</param>
|
||||
public void DeleteAllDataEntriesForWorld(string worldId)
|
||||
{
|
||||
sqlite.Execute("DELETE FROM data WHERE world_id = ?", worldId);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
sqlite.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using NLog;
|
||||
using NLog.Targets;
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
@@ -99,14 +100,12 @@ namespace VRCX
|
||||
// Layout with padding between the level/logger and message so that the message always starts at the same column
|
||||
Layout =
|
||||
"${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
|
||||
ArchiveFileName = Path.Join(AppDataDirectory, "logs", "VRCX.{#}.log"),
|
||||
ArchiveNumbering = ArchiveNumberingMode.DateAndSequence,
|
||||
ArchiveSuffixFormat = "{0:000}",
|
||||
ArchiveEvery = FileArchivePeriod.Day,
|
||||
MaxArchiveFiles = 4,
|
||||
MaxArchiveDays = 7,
|
||||
ArchiveAboveSize = 10000000,
|
||||
ArchiveOldFileOnStartup = true,
|
||||
ConcurrentWrites = true,
|
||||
KeepFileOpen = true,
|
||||
AutoFlush = true,
|
||||
Encoding = System.Text.Encoding.UTF8
|
||||
@@ -118,12 +117,13 @@ namespace VRCX
|
||||
Layout = "${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
|
||||
DetectConsoleAvailable = true
|
||||
};
|
||||
builder.ForLogger("VRCX").FilterMinLevel(LogLevel.Info).WriteTo(consoleTarget);
|
||||
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(consoleTarget);
|
||||
});
|
||||
}
|
||||
|
||||
#if !LINUX
|
||||
[STAThread]
|
||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
|
||||
private static void Main()
|
||||
{
|
||||
if (Wine.GetIfWine())
|
||||
@@ -150,7 +150,7 @@ namespace VRCX
|
||||
{
|
||||
case DialogResult.Yes:
|
||||
logger.Fatal("Handled Exception, user selected auto install of vc_redist.");
|
||||
Update.DownloadInstallRedist();
|
||||
Update.DownloadInstallRedist().GetAwaiter().GetResult();
|
||||
MessageBox.Show(
|
||||
"vc_redist has finished installing, if the issue persists upon next restart, please reinstall VRCX From GitHub,\nVRCX Will now restart.",
|
||||
"vc_redist installation complete", MessageBoxButtons.OK);
|
||||
@@ -210,6 +210,7 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("Interoperability", "CA1416:Validate platform compatibility")]
|
||||
private static void Run()
|
||||
{
|
||||
var args = Environment.GetCommandLineArgs();
|
||||
@@ -236,7 +237,6 @@ namespace VRCX
|
||||
AppApiVr.Instance.Init();
|
||||
ProcessMonitor.Instance.Init();
|
||||
Discord.Instance.Init();
|
||||
WorldDBManager.Instance.Init();
|
||||
WebApi.Instance.Init();
|
||||
LogWatcher.Instance.Init();
|
||||
AutoAppLaunchManager.Instance.Init();
|
||||
@@ -258,7 +258,6 @@ namespace VRCX
|
||||
AutoAppLaunchManager.Instance.Exit();
|
||||
LogWatcher.Instance.Exit();
|
||||
WebApi.Instance.Exit();
|
||||
WorldDBManager.Instance.Stop();
|
||||
|
||||
Discord.Instance.Exit();
|
||||
SystemMonitor.Instance.Exit();
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SharedVariable
|
||||
{
|
||||
public static readonly SharedVariable Instance;
|
||||
private readonly ReaderWriterLockSlim m_MapLock;
|
||||
private readonly Dictionary<string, string> m_Map;
|
||||
|
||||
static SharedVariable()
|
||||
{
|
||||
Instance = new SharedVariable();
|
||||
}
|
||||
|
||||
public SharedVariable()
|
||||
{
|
||||
m_MapLock = new ReaderWriterLockSlim();
|
||||
m_Map = new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
m_MapLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
m_Map.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_MapLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public string Get(string key)
|
||||
{
|
||||
m_MapLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (m_Map.TryGetValue(key, out string value) == true)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_MapLock.ExitReadLock();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Set(string key, string value)
|
||||
{
|
||||
m_MapLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
m_Map[key] = value;
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_MapLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Remove(string key)
|
||||
{
|
||||
m_MapLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return m_Map.Remove(key);
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_MapLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,12 +91,12 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CefSharp.OffScreen.NETCore" Version="137.0.100" />
|
||||
<PackageReference Include="CefSharp.WinForms.NETCore" Version="137.0.100" />
|
||||
<PackageReference Include="CefSharp.OffScreen.NETCore" Version="138.0.170" />
|
||||
<PackageReference Include="CefSharp.WinForms.NETCore" Version="138.0.170" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.3.0.28" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.5.0" />
|
||||
<PackageReference Include="NLog" Version="6.0.1" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
|
||||
@@ -104,10 +104,10 @@
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
|
||||
<PackageReference Include="System.Management" Version="9.0.6" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
|
||||
<PackageReference Include="System.Management" Version="9.0.7" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="Websocket.Client" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -93,12 +93,12 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.9.11" />
|
||||
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.11" />
|
||||
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.9.12" />
|
||||
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.12" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.3.0.28" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.5.0" />
|
||||
<PackageReference Include="NLog" Version="6.0.1" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
|
||||
@@ -106,10 +106,10 @@
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.6" />
|
||||
<PackageReference Include="System.Management" Version="9.0.6" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.7" />
|
||||
<PackageReference Include="System.Management" Version="9.0.7" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="Websocket.Client" Version="5.2.0" />
|
||||
</ItemGroup>
|
||||
@@ -121,8 +121,6 @@
|
||||
<Compile Remove="Cef\**" />
|
||||
<Content Remove="AppApi\Cef\**" />
|
||||
<Compile Remove="AppApi\Cef\**" />
|
||||
<Content Remove="PWI\**" />
|
||||
<Compile Remove="PWI\**" />
|
||||
<Content Remove="Overlay\**" />
|
||||
<Compile Remove="Overlay\**" />
|
||||
<Content Remove="DBMerger\**" />
|
||||
|
||||
Reference in New Issue
Block a user