Folder cs files

This commit is contained in:
Natsumi
2023-10-11 20:36:41 +13:00
parent 634f465927
commit 6cfadfa67f
53 changed files with 192 additions and 147 deletions
+1262
View File
File diff suppressed because it is too large Load Diff
+287
View File
@@ -0,0 +1,287 @@
// Copyright(c) 2019-2022 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;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Net;
using System.Text;
using System.ComponentModel;
using System.Linq;
namespace VRCX
{
public class AssetBundleCacher
{
public static readonly AssetBundleCacher Instance;
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
static AssetBundleCacher()
{
Instance = new AssetBundleCacher();
}
public static string DownloadTempLocation;
public static string DownloadDestinationLocation;
public static string DownloadHashLocation;
public static int DownloadProgress;
public static int DownloadSize;
public static bool DownloadCanceled;
public static WebClient client;
public static Process process;
public string GetAssetId(string id)
{
using(var sha256 = SHA256.Create())
{
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(id));
StringBuilder idHex = new StringBuilder(hash.Length * 2);
foreach (byte b in hash)
{
idHex.AppendFormat("{0:x2}", b);
}
return idHex.ToString().ToUpper().Substring(0, 16);
}
}
public string GetAssetVersion(int version)
{
byte[] bytes = BitConverter.GetBytes(version);
string versionHex = string.Empty;
foreach (byte b in bytes)
{
versionHex += b.ToString("X2");
}
return versionHex.PadLeft(32, '0');
}
public string 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)
{
var cachePath = GetVRChatCacheLocation();
var idHash = GetAssetId(id);
var versionLocation = GetAssetVersion(version);
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>A Tuple containing the file size, lock status and path of the asset bundle.</returns>
public Tuple<long, bool, string> CheckVRChatCache(string id, int version)
{
long fileSize = -1;
var isLocked = false;
var fullLocation = GetVRChatCacheFullLocation(id, version);
var fileLocation = Path.Combine(fullLocation, "__data");
var cachePath = string.Empty;
if (File.Exists(fileLocation))
{
cachePath = fullLocation;
FileInfo data = new FileInfo(fileLocation);
fileSize = data.Length;
}
if (File.Exists(Path.Combine(fullLocation, "__lock")))
{
isLocked = true;
}
return new Tuple<long, bool, string>(fileSize, isLocked, cachePath);
}
// old asset bundle cacher downloader method reused for updating, it's not pretty
public void DownloadFile(string fileUrl, string hashUrl, int size)
{
client = new WebClient();
client.Headers.Add("user-agent", Program.Version);
DownloadProgress = 0;
DownloadSize = size;
DownloadCanceled = false;
DownloadTempLocation = Path.Combine(Program.AppDataDirectory, "tempDownload.exe");
DownloadDestinationLocation = Path.Combine(Program.AppDataDirectory, "update.exe");
DownloadHashLocation = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
if (File.Exists(DownloadHashLocation))
File.Delete(DownloadHashLocation);
if (!string.IsNullOrEmpty(hashUrl))
client.DownloadFile(new Uri(hashUrl), DownloadHashLocation);
client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(DownloadProgressCallback);
client.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompletedCallback);
client.DownloadFileAsync(new Uri(fileUrl), DownloadTempLocation);
}
public void CancelDownload()
{
DownloadCanceled = true;
try
{
client?.CancelAsync();
if (File.Exists(DownloadTempLocation))
File.Delete(DownloadTempLocation);
}
catch (Exception)
{
}
DownloadProgress = -4;
}
public int CheckDownloadProgress()
{
return DownloadProgress;
}
private static void DownloadProgressCallback(object sender, DownloadProgressChangedEventArgs e)
{
DownloadProgress = e.ProgressPercentage;
}
private static void DownloadCompletedCallback(object sender, AsyncCompletedEventArgs e)
{
if (DownloadCanceled)
{
if (File.Exists(DownloadTempLocation))
File.Delete(DownloadTempLocation);
return;
}
if (!File.Exists(DownloadTempLocation))
{
DownloadProgress = -15;
return;
}
FileInfo data = new FileInfo(DownloadTempLocation);
if (data.Length != DownloadSize)
{
File.Delete(DownloadTempLocation);
DownloadProgress = -15;
return;
}
if (File.Exists(DownloadHashLocation))
{
logger.Info("Updater: Checking hash");
var lines = File.ReadAllLines(DownloadHashLocation);
var hash = lines.Length > 0 ? lines[0].Split(' ') : new[] { "" };
using (var sha256 = SHA256.Create())
using (var stream = File.OpenRead(DownloadTempLocation))
{
var hashBytes = sha256.ComputeHash(stream);
var hashString = BitConverter.ToString(hashBytes).Replace("-", "");
if (!hashString.Equals(hash[0], StringComparison.OrdinalIgnoreCase))
{
logger.Error($"Updater: Hash check failed file:{hashString} remote:{hash[0]}");
// can't delete file yet because it's in use
DownloadProgress = -14;
return;
}
}
logger.Info("Updater: Hash check passed");
}
if (File.Exists(DownloadDestinationLocation))
File.Delete(DownloadDestinationLocation);
File.Move(DownloadTempLocation, DownloadDestinationLocation);
DownloadProgress = -16;
}
/// <summary>
/// Deletes the cache directory for a specific asset bundle.
/// </summary>
/// <param name="id">The ID of the asset bundle to delete.</param>
/// <param name="version">The version of the asset bundle to delete.</param>
public void DeleteCache(string id, int version)
{
var FullLocation = GetVRChatCacheFullLocation(id, version);
if (Directory.Exists(FullLocation))
Directory.Delete(FullLocation, true);
}
/// <summary>
/// Deletes the entire VRChat cache directory.
/// </summary>
public void DeleteAllCache()
{
var cachePath = GetVRChatCacheLocation();
if (Directory.Exists(cachePath))
{
Directory.Delete(cachePath, true);
Directory.CreateDirectory(cachePath);
}
}
/// <summary>
/// Removes empty directories from the VRChat cache directory and deletes old versions of cached asset bundles.
/// </summary>
public void SweepCache()
{
var cachePath = GetVRChatCacheLocation();
if (!Directory.Exists(cachePath))
return;
var directories = new DirectoryInfo(cachePath);
DirectoryInfo[] cacheDirectories = directories.GetDirectories();
foreach (DirectoryInfo cacheDirectory in cacheDirectories)
{
var VersionDirectories = cacheDirectory.GetDirectories().OrderBy(d => Convert.ToInt32(d.Name, 16));
int i = 0;
foreach (DirectoryInfo VersionDirectory in VersionDirectories)
{
i++;
if (VersionDirectory.GetDirectories().Length + VersionDirectory.GetFiles().Length == 0)
{
VersionDirectory.Delete();
}
else if (i < VersionDirectories.Count())
{
if (!File.Exists(Path.Combine(VersionDirectory.FullName, "__lock")))
VersionDirectory.Delete(true);
}
}
if (cacheDirectory.GetDirectories().Length + cacheDirectory.GetFiles().Length == 0)
cacheDirectory.Delete();
}
}
/// <summary>
/// Returns the size of the VRChat cache directory in bytes.
/// </summary>
/// <returns>The size of the VRChat cache directory in bytes.</returns>
public long GetCacheSize()
{
var cachePath = GetVRChatCacheLocation();
if (!Directory.Exists(cachePath)) return 0;
return DirSize(new DirectoryInfo(cachePath));
}
/// <summary>
/// Recursively calculates the size of a directory and all its subdirectories.
/// </summary>
/// <param name="d">The directory to calculate the size of.</param>
/// <returns>The size of the directory and all its subdirectories in bytes.</returns>
public long DirSize(DirectoryInfo d)
{
long size = 0;
FileInfo[] files = d.GetFiles("*.*", SearchOption.AllDirectories);
foreach (FileInfo file in files)
{
size += file.Length;
}
return size;
}
}
}
+337
View File
@@ -0,0 +1,337 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Timers;
namespace VRCX
{
/// <summary>
/// The class responsible for launching user-defined applications when VRChat opens/closes.
/// </summary>
public class AutoAppLaunchManager
{
public static AutoAppLaunchManager Instance { get; private set; }
public static readonly string VRChatProcessName = "VRChat";
public bool Enabled = false;
/// <summary> Whether or not to kill child processes when VRChat closes. </summary>
public bool KillChildrenOnExit = true;
public readonly string AppShortcutDirectory;
private DateTime startTime = DateTime.Now;
private Dictionary<string, HashSet<int>> startedProcesses = new Dictionary<string, HashSet<int>>();
private readonly Timer childUpdateTimer;
private int timerTicks = 0;
private static readonly byte[] shortcutSignatureBytes = { 0x4C, 0x00, 0x00, 0x00 }; // signature for ShellLinkHeader
private const uint TH32CS_SNAPPROCESS = 2;
// Requires access rights 'PROCESS_QUERY_INFORMATION' and 'PROCESS_VM_READ'
[DllImport("kernel32.dll")]
public static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
[DllImport("kernel32.dll")]
public static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
[DllImport("kernel32.dll")]
public static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
[StructLayout(LayoutKind.Sequential)]
public struct PROCESSENTRY32
{
public uint dwSize;
public uint cntUsage;
public uint th32ProcessID;
public IntPtr th32DefaultHeapID;
public uint th32ModuleID;
public uint cntThreads;
public uint th32ParentProcessID;
public int pcPriClassBase;
public uint dwFlags;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szExeFile;
};
static AutoAppLaunchManager()
{
Instance = new AutoAppLaunchManager();
}
public AutoAppLaunchManager()
{
AppShortcutDirectory = Path.Combine(Program.AppDataDirectory, "startup");
if (!Directory.Exists(AppShortcutDirectory))
{
Directory.CreateDirectory(AppShortcutDirectory);
}
ProcessMonitor.Instance.ProcessStarted += OnProcessStarted;
ProcessMonitor.Instance.ProcessExited += OnProcessExited;
childUpdateTimer = new Timer();
childUpdateTimer.Interval = 60000;
childUpdateTimer.Elapsed += ChildUpdateTimer_Elapsed;
}
private void OnProcessExited(MonitoredProcess monitoredProcess)
{
if (!monitoredProcess.HasName(VRChatProcessName))
return;
lock (startedProcesses)
{
if (KillChildrenOnExit)
{
childUpdateTimer.Stop();
KillChildProcesses();
}
else
UpdateChildProcesses();
}
}
private void OnProcessStarted(MonitoredProcess monitoredProcess)
{
if (!Enabled || !monitoredProcess.HasName(VRChatProcessName) || monitoredProcess.Process.StartTime < startTime)
return;
lock (startedProcesses)
{
if (KillChildrenOnExit)
KillChildProcesses();
else
UpdateChildProcesses();
var shortcutFiles = FindShortcutFiles(AppShortcutDirectory);
foreach (var file in shortcutFiles)
{
if (!IsChildProcessRunning(file))
StartChildProcess(file);
}
if (shortcutFiles.Length == 0)
return;
timerTicks = 0;
childUpdateTimer.Interval = 1000;
childUpdateTimer.Start();
}
}
/// <summary>
/// Kills all running child processes.
/// </summary>
internal void KillChildProcesses()
{
UpdateChildProcesses(); // Ensure the list contains all current child processes.
foreach (var pair in startedProcesses)
{
var processes = pair.Value;
foreach (var pid in processes)
{
if (!WinApi.HasProcessExited(pid))
KillProcessTree(pid);
}
}
startedProcesses.Clear();
}
// TODO: Proper error handling for winapi calls.
// Will fail if process is protected, has admin rights, user account is limited(?), or process is dead
// catch win32exceptions
// This is a recursive function that kills a process and all of its children.
// It uses the CreateToolhelp32Snapshot winapi func to get a snapshot of all running processes, loops through them with Process32First/Process32Next, and kills any processes that have the given pid as their parent.
/// <summary>
/// Returns the child processes of a process.
/// </summary>
/// <param name="pid">The process ID of the parent process.</param>
public static List<int> FindChildProcesses(int pid, bool recursive = true)
{
List<int> pids = new List<int>();
IntPtr snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == IntPtr.Zero)
{
return pids;
}
// Gonna be honest, not gonna spin up a 32bit windows VM to make sure this works. but it should.
// Does VRCX even run on 32bit windows?
PROCESSENTRY32 procEntry = new PROCESSENTRY32();
procEntry.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));
if (Process32First(snapshot, ref procEntry))
{
do
{
if (procEntry.th32ParentProcessID == pid)
{
pids.Add((int)procEntry.th32ProcessID);
if(recursive) // Recursively find child processes
pids.AddRange(FindChildProcesses((int)procEntry.th32ProcessID));
}
}
while (Process32Next(snapshot, ref procEntry));
}
return pids;
}
/// <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)
{
var pids = FindChildProcesses(pid);
pids.Add(pid); // Kill parent
foreach (int p in pids)
{
try
{
using (Process proc = Process.GetProcessById(p))
proc.Kill();
}
catch
{
}
}
}
/// <summary>
/// Starts a new child process.
/// </summary>
/// <param name="path">The path.</param>
internal void StartChildProcess(string path)
{
try
{
using (var process = Process.Start(path))
if (process != null)
startedProcesses.Add(path, new HashSet<int>() { process.Id });
} catch { }
}
/// <summary>
/// Updates the child processes list.
/// Removes any processes that have exited.
/// </summary>
internal void UpdateChildProcesses()
{
foreach (var pair in startedProcesses.ToArray())
{
var processes = pair.Value;
foreach (var pid in processes.ToArray())
{
bool recursiveChildSearch = processes.Count == 1; // Disable recursion when this list may already contain the entire process tree
var childProcesses = FindChildProcesses(pid, recursiveChildSearch);
foreach (int childPid in childProcesses) // Monitor child processes
processes.Add(childPid); // HashSet will prevent duplication
if (WinApi.HasProcessExited(pid))
processes.Remove(pid);
}
if (processes.Count == 0) // All processes associated with the shortcut have exited.
startedProcesses.Remove(pair.Key);
}
}
/// <summary>
/// Checks to see if a given file matches a current running child process.
/// </summary>
/// <param name="path">The path.</param>
/// <returns>
/// <c>true</c> if child process running; otherwise, <c>false</c>.
/// </returns>
internal bool IsChildProcessRunning(string path)
{
return startedProcesses.ContainsKey(path);
}
internal void Init()
{
// What are you lookin at? :eyes:
}
internal void Exit()
{
childUpdateTimer.Stop();
Enabled = false;
// people thought this behavior was a bug
// lock (startedProcesses)
// KillChildProcesses();
}
private void ChildUpdateTimer_Elapsed(object sender, ElapsedEventArgs e)
{
lock (startedProcesses)
UpdateChildProcesses();
if (timerTicks < 5)
{
timerTicks++;
if(timerTicks == 5)
childUpdateTimer.Interval = 60000;
}
}
/// <summary>
/// Finds windows shortcut files in a given folder.
/// </summary>
/// <param name="folderPath">The folder path.</param>
/// <returns>An array of shortcut paths. If none, then empty.</returns>
private static string[] FindShortcutFiles(string folderPath)
{
DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
FileInfo[] files = directoryInfo.GetFiles();
List<string> ret = new List<string>();
foreach (FileInfo file in files)
{
if (IsShortcutFile(file.FullName))
{
ret.Add(file.FullName);
}
}
return ret.ToArray();
}
/// <summary>
/// Determines whether the specified file path is a shortcut by checking the file header.
/// </summary>
/// <param name="filePath">The file path.</param>
/// <returns><c>true</c> if the given file path is a shortcut, otherwise <c>false</c></returns>
private static bool IsShortcutFile(string filePath)
{
byte[] headerBytes = new byte[4];
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
if (fileStream.Length >= 4)
{
fileStream.Read(headerBytes, 0, 4);
}
}
return headerBytes.SequenceEqual(shortcutSignatureBytes);
}
}
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright(c) 2019-2022 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 CefSharp;
namespace VRCX
{
public class CustomDownloadHandler : IDownloadHandler
{
public bool CanDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, string url, string requestMethod)
{
return true;
}
public void OnBeforeDownload(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IBeforeDownloadCallback callback)
{
if (callback.IsDisposed)
return;
using (callback)
{
callback.Continue(
downloadItem.SuggestedFileName,
showDialog: true
);
}
}
public void OnDownloadUpdated(IWebBrowser chromiumWebBrowser, IBrowser browser, DownloadItem downloadItem, IDownloadItemCallback callback)
{
}
}
}
+43
View File
@@ -0,0 +1,43 @@
using CefSharp.Enums;
using CefSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace VRCX
{
/// <summary>
/// This class is used to 'handle' drag and drop events.
/// All it does is call a function in the app with the file name of the file being dragged into the window, since chromium doesn't have access to the full file path on regular drop events.
/// </summary>
public class CustomDragHandler : IDragHandler
{
public bool OnDragEnter(IWebBrowser chromiumWebBrowser, IBrowser browser, IDragData dragData, DragOperationsMask mask)
{
if (dragData.IsFile && dragData.FileNames != null && dragData.FileNames.Count > 0)
{
string file = dragData.FileNames[0];
if (!file.EndsWith(".png") && !file.EndsWith(".jpg") && !file.EndsWith(".jpeg"))
{
dragData.Dispose();
return true;
}
// forgive me father for i have sinned once again
AppApi.Instance.ExecuteAppFunction("dragEnterCef", dragData.FileNames[0]);
dragData.Dispose();
return false;
}
dragData.Dispose();
return true;
}
public void OnDraggableRegionsChanged(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IList<DraggableRegion> regions)
{
}
}
}
+33
View File
@@ -0,0 +1,33 @@
// Copyright(c) 2019 pypy. All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
using CefSharp;
namespace VRCX
{
public class CustomMenuHandler : IContextMenuHandler
{
public void OnBeforeContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model)
{
// remove default right click
if (!parameters.TypeFlags.HasFlag(ContextMenuType.Selection) && !parameters.TypeFlags.HasFlag(ContextMenuType.Editable))
model.Clear();
}
public bool OnContextMenuCommand(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, CefMenuCommand commandId, CefEventFlags eventFlags)
{
return false;
}
public void OnContextMenuDismissed(IWebBrowser browserControl, IBrowser browser, IFrame frame)
{
}
public bool RunContextMenu(IWebBrowser browserControl, IBrowser browser, IFrame frame, IContextMenuParams parameters, IMenuModel model, IRunContextMenuCallback callback)
{
return false;
}
}
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright(c) 2019-2022 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 CefSharp;
using CefSharp.Enums;
namespace VRCX
{
public class CefNoopDragHandler : IDragHandler
{
bool IDragHandler.OnDragEnter(IWebBrowser chromiumWebBrowser, IBrowser browser, IDragData dragData, DragOperationsMask mask)
{
return true;
}
void IDragHandler.OnDraggableRegionsChanged(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IList<DraggableRegion> regions)
{
}
}
}
+73
View File
@@ -0,0 +1,73 @@
using System;
using System.IO;
using CefSharp;
using CefSharp.SchemeHandler;
using CefSharp.WinForms;
namespace VRCX
{
public class CefService
{
public static readonly CefService Instance;
static CefService()
{
Instance = new CefService();
}
internal void Init()
{
var userDataDir = Path.Combine(Program.AppDataDirectory, "userdata");
var cefSettings = new CefSettings
{
RootCachePath = userDataDir,
CachePath = Path.Combine(userDataDir, "cache"),
LogSeverity = LogSeverity.Disable,
WindowlessRenderingEnabled = true,
PersistSessionCookies = true,
PersistUserPreferences = true,
UserAgent = Program.Version
};
cefSettings.RegisterScheme(new CefCustomScheme
{
SchemeName = "file",
DomainName = "vrcx",
SchemeHandlerFactory = new FolderSchemeHandlerFactory(
Path.Combine(Program.BaseDirectory, "html"),
"file",
defaultPage: "index.html"
),
IsLocal = true
});
// cefSettings.CefCommandLineArgs.Add("allow-universal-access-from-files");
// cefSettings.CefCommandLineArgs.Add("ignore-certificate-errors");
// cefSettings.CefCommandLineArgs.Add("disable-plugins");
cefSettings.CefCommandLineArgs.Add("disable-spell-checking");
cefSettings.CefCommandLineArgs.Add("disable-pdf-extension");
cefSettings.CefCommandLineArgs["autoplay-policy"] = "no-user-gesture-required";
cefSettings.CefCommandLineArgs.Add("disable-web-security");
cefSettings.SetOffScreenRenderingBestPerformanceArgs(); // causes white screen sometimes?
if (Program.LaunchDebug)
{
cefSettings.RemoteDebuggingPort = 8088;
cefSettings.CefCommandLineArgs["remote-allow-origins"] = "*";
}
CefSharpSettings.WcfEnabled = true; // TOOD: REMOVE THIS LINE YO (needed for synchronous configRepository)
CefSharpSettings.ShutdownOnExit = false;
if (Cef.Initialize(cefSettings) == false)
{
throw new Exception("Cef.Initialize()");
}
}
internal void Exit()
{
Cef.Shutdown();
}
}
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright(c) 2019-2022 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.Diagnostics;
using System.Threading;
namespace VRCX
{
public class CpuMonitor
{
public static readonly CpuMonitor Instance;
public float CpuUsage;
private readonly PerformanceCounter _performanceCounter;
private readonly Timer _timer;
static CpuMonitor()
{
Instance = new CpuMonitor();
}
public CpuMonitor()
{
try
{
_performanceCounter = new PerformanceCounter(
"Processor Information",
"% Processor Utility",
"_Total",
true
);
}
catch
{
}
// fallback
if (_performanceCounter == null)
{
try
{
_performanceCounter = new PerformanceCounter(
"Processor",
"% Processor Time",
"_Total",
true
);
}
catch
{
}
}
_timer = new Timer(TimerCallback, null, -1, -1);
}
internal void Init()
{
_timer.Change(1000, 1000);
}
internal void Exit()
{
lock (this)
{
_timer.Change(-1, -1);
_performanceCounter?.Dispose();
}
}
private void TimerCallback(object state)
{
lock (this)
{
try
{
if (_performanceCounter != null)
{
CpuUsage = _performanceCounter.NextValue();
}
}
catch
{
}
}
}
}
}
+222
View File
@@ -0,0 +1,222 @@
// Copyright(c) 2019-2022 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 DiscordRPC;
using System.Text;
using System.Threading;
namespace VRCX
{
public class Discord
{
public static readonly Discord Instance;
private readonly ReaderWriterLockSlim m_Lock;
private readonly RichPresence m_Presence;
private DiscordRpcClient m_Client;
private readonly Timer m_Timer;
private bool m_Active;
public static string DiscordAppId;
static Discord()
{
Instance = new Discord();
}
public Discord()
{
m_Lock = new ReaderWriterLockSlim();
m_Presence = new RichPresence();
m_Timer = new Timer(TimerCallback, null, -1, -1);
}
internal void Init()
{
m_Timer.Change(0, 1000);
}
internal void Exit()
{
lock (this)
{
m_Timer.Change(-1, -1);
m_Client?.Dispose();
}
}
private void TimerCallback(object state)
{
lock (this)
{
try
{
Update();
}
catch
{
}
}
}
private void Update()
{
if (m_Client == null && m_Active)
{
m_Client = new DiscordRpcClient(DiscordAppId);
if (m_Client.Initialize() == false)
{
m_Client.Dispose();
m_Client = null;
}
}
if (m_Client != null && !m_Active)
{
m_Client.Dispose();
m_Client = null;
}
if (m_Client != null && !m_Lock.IsWriteLockHeld)
{
m_Lock.EnterReadLock();
try
{
m_Client.SetPresence(m_Presence);
}
finally
{
m_Lock.ExitReadLock();
}
m_Client.Invoke();
}
}
public bool SetActive(bool active)
{
m_Active = active;
return m_Active;
}
// https://stackoverflow.com/questions/1225052/best-way-to-shorten-utf8-string-based-on-byte-length
private static string LimitByteLength(string str, int maxBytesLength)
{
if (str == null)
return string.Empty;
var bytesArr = Encoding.UTF8.GetBytes(str);
var bytesToRemove = 0;
var lastIndexInString = str.Length - 1;
while (bytesArr.Length - bytesToRemove > maxBytesLength)
{
bytesToRemove += Encoding.UTF8.GetByteCount(new char[] { str[lastIndexInString] });
--lastIndexInString;
}
return Encoding.UTF8.GetString(bytesArr, 0, bytesArr.Length - bytesToRemove);
}
public void SetText(string details, string state)
{
if (m_Client != null && !m_Lock.IsReadLockHeld)
{
m_Lock.EnterWriteLock();
try
{
m_Presence.Details = LimitByteLength(details, 127);
m_Presence.State = LimitByteLength(state, 127);
}
finally
{
m_Lock.ExitWriteLock();
}
}
}
public void SetAssets(string largeKey, string largeText, string smallKey, string smallText, string partyId, int partySize, int partyMax, string buttonText, string buttonUrl, string appId)
{
m_Lock.EnterWriteLock();
try
{
if (string.IsNullOrEmpty(largeKey) == true &&
string.IsNullOrEmpty(smallKey) == true)
{
m_Presence.Assets = null;
}
else
{
if (m_Presence.Assets == null)
m_Presence.Assets = new Assets();
if (m_Presence.Party == null)
m_Presence.Party = new Party();
m_Presence.Assets.LargeImageKey = largeKey;
m_Presence.Assets.LargeImageText = largeText;
m_Presence.Assets.SmallImageKey = smallKey;
m_Presence.Assets.SmallImageText = smallText;
m_Presence.Party.ID = partyId;
m_Presence.Party.Size = partySize;
m_Presence.Party.Max = partyMax;
Button[] Buttons = { };
if (!string.IsNullOrEmpty(buttonUrl))
{
Buttons = new Button[]
{
new Button() { Label = buttonText, Url = buttonUrl }
};
}
m_Presence.Buttons = Buttons;
if (DiscordAppId != appId)
{
DiscordAppId = appId;
if (m_Client != null)
{
m_Client.Dispose();
m_Client = null;
}
Update();
}
}
}
finally
{
m_Lock.ExitWriteLock();
}
}
public void SetTimestamps(double startUnixMilliseconds, double endUnixMilliseconds)
{
var _startUnixMilliseconds = (ulong)startUnixMilliseconds;
var _endUnixMilliseconds = (ulong)endUnixMilliseconds;
m_Lock.EnterWriteLock();
try
{
if (_startUnixMilliseconds == 0)
{
m_Presence.Timestamps = null;
}
else
{
if (m_Presence.Timestamps == null)
{
m_Presence.Timestamps = new Timestamps();
}
m_Presence.Timestamps.StartUnixMilliseconds = _startUnixMilliseconds;
if (_endUnixMilliseconds == 0)
{
m_Presence.Timestamps.End = null;
}
else
{
m_Presence.Timestamps.EndUnixMilliseconds = _endUnixMilliseconds;
}
}
}
finally
{
m_Lock.ExitWriteLock();
}
}
}
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright(c) 2019-2022 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;
using System.Globalization;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading.Tasks;
using CefSharp;
using Newtonsoft.Json;
namespace VRCX
{
internal class IPCClient
{
private static readonly UTF8Encoding noBomEncoding = new UTF8Encoding(false, false);
private readonly NamedPipeServerStream _ipcServer;
private readonly byte[] _recvBuffer = new byte[1024 * 8];
private readonly MemoryStream memoryStream;
private readonly byte[] packetBuffer = new byte[1024 * 1024];
private readonly Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
private string _currentPacket;
public IPCClient(NamedPipeServerStream ipcServer)
{
memoryStream = new MemoryStream(packetBuffer);
serializer.Culture = CultureInfo.InvariantCulture;
serializer.Formatting = Formatting.None;
_ipcServer = ipcServer;
}
public void BeginRead()
{
_ipcServer.BeginRead(_recvBuffer, 0, _recvBuffer.Length, OnRead, _ipcServer);
}
public void Send(IPCPacket ipcPacket)
{
try
{
memoryStream.Seek(0, SeekOrigin.Begin);
using (var streamWriter = new StreamWriter(memoryStream, noBomEncoding, 65535, true))
using (var writer = new JsonTextWriter(streamWriter))
{
serializer.Serialize(writer, ipcPacket);
streamWriter.Write((char)0x00);
streamWriter.Flush();
}
var length = (int)memoryStream.Position;
_ipcServer?.BeginWrite(packetBuffer, 0, length, OnSend, null);
}
catch
{
IPCServer.Clients.Remove(this);
}
}
private void OnRead(IAsyncResult asyncResult)
{
try
{
var bytesRead = _ipcServer.EndRead(asyncResult);
if (bytesRead <= 0)
{
IPCServer.Clients.Remove(this);
_ipcServer.Close();
return;
}
_currentPacket += Encoding.UTF8.GetString(_recvBuffer, 0, bytesRead);
if (_currentPacket[_currentPacket.Length - 1] == (char)0x00)
{
var packets = _currentPacket.Split((char)0x00);
foreach (var packet in packets)
{
if (string.IsNullOrEmpty(packet))
continue;
MainForm.Instance.Browser.ExecuteScriptAsync("$app.ipcEvent", packet);
}
_currentPacket = string.Empty;
}
}
catch (Exception e)
{
Console.WriteLine(e);
}
BeginRead();
}
public static void OnSend(IAsyncResult asyncResult)
{
var ipcClient = (NamedPipeClientStream)asyncResult.AsyncState;
ipcClient?.EndWrite(asyncResult);
}
public static void Close(IAsyncResult asyncResult)
{
var ipcClient = (NamedPipeClientStream)asyncResult.AsyncState;
ipcClient?.EndWrite(asyncResult);
ipcClient?.Close();
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace VRCX
{
public class IPCPacket
{
public string Type { get; set; }
public string Data { get; set; }
public string MsgType { get; set; }
}
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright(c) 2019-2022 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;
using System.Collections.Generic;
using System.IO.Pipes;
using System.Threading.Tasks;
namespace VRCX
{
internal class IPCServer
{
public static readonly IPCServer Instance;
public static readonly List<IPCClient> Clients = new List<IPCClient>();
static IPCServer()
{
Instance = new IPCServer();
}
public void Init()
{
new IPCServer().CreateIPCServer();
}
public static void Send(IPCPacket ipcPacket)
{
foreach (var client in Clients)
{
client.Send(ipcPacket);
}
}
public void CreateIPCServer()
{
var ipcServer = new NamedPipeServerStream("vrcx-ipc", PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
ipcServer.BeginWaitForConnection(DoAccept, ipcServer);
}
private void DoAccept(IAsyncResult asyncResult)
{
var ipcServer = (NamedPipeServerStream)asyncResult.AsyncState;
try
{
ipcServer.EndWaitForConnection(asyncResult);
}
catch (Exception e)
{
Console.WriteLine(e);
}
var ipcClient = new IPCClient(ipcServer);
Clients.Add(ipcClient);
ipcClient.BeginRead();
CreateIPCServer();
}
}
}
+65
View File
@@ -0,0 +1,65 @@
using System;
using System.IO;
using System.Net;
using System.Linq;
namespace VRCX
{
class ImageCache
{
private static readonly string cacheLocation = Path.Combine(Program.AppDataDirectory, "ImageCache");
private static readonly WebClient webClient = new WebClient();
private const string IMAGE_HOST1 = "api.vrchat.cloud";
private const string IMAGE_HOST2 = "files.vrchat.cloud";
private const string IMAGE_HOST3 = "d348imysud55la.cloudfront.net";
public static string GetImage(string url, string fileId, string version)
{
var directoryLocation = Path.Combine(cacheLocation, fileId);
var fileLocation = Path.Combine(directoryLocation, $"{version}.png");
if (File.Exists(fileLocation))
{
Directory.SetLastWriteTimeUtc(directoryLocation, DateTime.UtcNow);
return fileLocation;
}
if (Directory.Exists(directoryLocation))
Directory.Delete(directoryLocation, true);
Directory.CreateDirectory(directoryLocation);
Uri uri = new Uri(url);
if (uri.Host != IMAGE_HOST1 && uri.Host != IMAGE_HOST2 && uri.Host != IMAGE_HOST3)
throw new ArgumentException("Invalid image host", url);
string cookieString = string.Empty;
if (WebApi.Instance != null && WebApi.Instance._cookieContainer != null)
{
CookieCollection cookies = WebApi.Instance._cookieContainer.GetCookies(new Uri($"https://{IMAGE_HOST1}"));
foreach (Cookie cookie in cookies)
cookieString += $"{cookie.Name}={cookie.Value};";
}
webClient.Headers.Add(HttpRequestHeader.Cookie, cookieString);
webClient.Headers.Add("user-agent", Program.Version);
webClient.DownloadFile(url, fileLocation);
int cacheSize = Directory.GetDirectories(cacheLocation).Length;
if (cacheSize > 1100)
CleanImageCache();
return fileLocation;
}
private static void CleanImageCache()
{
DirectoryInfo dirInfo = new DirectoryInfo(cacheLocation);
var folders = dirInfo.GetDirectories().OrderByDescending(p => p.LastWriteTime).Skip(1000);
foreach (DirectoryInfo folder in folders)
{
folder.Delete(true);
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright(c) 2019-2022 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 Newtonsoft.Json;
using System.IO;
using System.Text;
namespace VRCX
{
public static class JsonSerializer
{
public static void Serialize<T>(string path, T obj)
{
try
{
using (var file = File.Open(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite))
using (var stream = new StreamWriter(file, Encoding.UTF8))
using (var writer = new JsonTextWriter(stream))
{
var serializer = Newtonsoft.Json.JsonSerializer.CreateDefault();
serializer.Formatting = Formatting.Indented;
serializer.Serialize(writer, obj, typeof(T));
}
}
catch
{
}
}
public static bool Deserialize<T>(string path, ref T obj) where T : new()
{
try
{
using (var file = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var stream = new StreamReader(file, Encoding.UTF8))
using (var reader = new JsonTextReader(stream))
{
var o = Newtonsoft.Json.JsonSerializer.CreateDefault().Deserialize<T>(reader);
if (o == null)
{
o = new T();
}
obj = o;
return true;
}
}
catch
{
}
return false;
}
}
}
+1171
View File
File diff suppressed because it is too large Load Diff
+117
View File
@@ -0,0 +1,117 @@
// Copyright(c) 2019 pypy. All rights reserved.
//
// This work is licensed under the terms of the MIT license.
// For a copy, see <https://opensource.org/licenses/MIT>.
namespace VRCX
{
partial class MainForm
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.TrayMenu = new System.Windows.Forms.ContextMenuStrip(this.components);
this.TrayMenu_Open = new System.Windows.Forms.ToolStripMenuItem();
this.TrayMenu_DevTools = new System.Windows.Forms.ToolStripMenuItem();
this.TrayMenu_Separator = new System.Windows.Forms.ToolStripSeparator();
this.TrayMenu_Quit = new System.Windows.Forms.ToolStripMenuItem();
this.TrayIcon = new System.Windows.Forms.NotifyIcon(this.components);
this.TrayMenu.SuspendLayout();
this.SuspendLayout();
//
// TrayMenu
//
this.TrayMenu.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.TrayMenu_Open,
this.TrayMenu_DevTools,
this.TrayMenu_Separator,
this.TrayMenu_Quit});
this.TrayMenu.Name = "TrayMenu";
this.TrayMenu.Size = new System.Drawing.Size(132, 54);
//
// TrayMenu_Open
//
this.TrayMenu_Open.Name = "TrayMenu_Open";
this.TrayMenu_Open.Size = new System.Drawing.Size(131, 22);
this.TrayMenu_Open.Text = "Open";
this.TrayMenu_Open.Click += new System.EventHandler(this.TrayMenu_Open_Click);
//
// TrayMenu_DevTools
//
this.TrayMenu_DevTools.Name = "TrayMenu_DevTools";
this.TrayMenu_DevTools.Size = new System.Drawing.Size(131, 22);
this.TrayMenu_DevTools.Text = "DevTools";
this.TrayMenu_DevTools.Click += new System.EventHandler(this.TrayMenu_DevTools_Click);
//
// TrayMenu_Separator
//
this.TrayMenu_Separator.Name = "TrayMenu_Separator";
this.TrayMenu_Separator.Size = new System.Drawing.Size(128, 6);
//
// TrayMenu_Quit
//
this.TrayMenu_Quit.Name = "TrayMenu_Quit";
this.TrayMenu_Quit.Size = new System.Drawing.Size(131, 22);
this.TrayMenu_Quit.Text = "Quit VRCX";
this.TrayMenu_Quit.Click += new System.EventHandler(this.TrayMenu_Quit_Click);
//
// TrayIcon
//
this.TrayIcon.ContextMenuStrip = this.TrayMenu;
this.TrayIcon.Text = "VRCX";
this.TrayIcon.Visible = true;
this.TrayIcon.MouseClick += new System.Windows.Forms.MouseEventHandler(this.TrayIcon_MouseClick);
//
// MainForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(842, 561);
this.MinimumSize = new System.Drawing.Size(320, 240);
this.Name = "MainForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = Program.Version;
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.MainForm_FormClosing);
this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.MainForm_FormClosed);
this.Load += new System.EventHandler(this.MainForm_Load);
this.Move += new System.EventHandler(this.MainForm_Move);
this.Resize += new System.EventHandler(this.MainForm_Resize);
this.TrayMenu.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.ContextMenuStrip TrayMenu;
private System.Windows.Forms.ToolStripMenuItem TrayMenu_Open;
private System.Windows.Forms.ToolStripMenuItem TrayMenu_DevTools;
private System.Windows.Forms.ToolStripSeparator TrayMenu_Separator;
private System.Windows.Forms.ToolStripMenuItem TrayMenu_Quit;
private System.Windows.Forms.NotifyIcon TrayIcon;
}
}
+201
View File
@@ -0,0 +1,201 @@
// Copyright(c) 2019-2022 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;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
namespace VRCX
{
public partial class MainForm : WinformBase
{
public static MainForm Instance;
private static NLog.Logger jslogger = NLog.LogManager.GetLogger("Javascript");
public ChromiumWebBrowser Browser;
private int LastLocationX;
private int LastLocationY;
private int LastSizeWidth;
private int LastSizeHeight;
public MainForm()
{
Instance = this;
InitializeComponent();
try
{
var location = Assembly.GetExecutingAssembly().Location;
var icon = Icon.ExtractAssociatedIcon(location);
Icon = icon;
TrayIcon.Icon = icon;
}
catch
{
}
Browser = new ChromiumWebBrowser("file://vrcx/index.html")
{
DragHandler = new CustomDragHandler(),
MenuHandler = new CustomMenuHandler(),
DownloadHandler = new CustomDownloadHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
Browser.IsBrowserInitializedChanged += (A, B) =>
{
if (Program.LaunchDebug)
Browser.ShowDevTools();
};
Util.ApplyJavascriptBindings(Browser.JavascriptObjectRepository);
Browser.ConsoleMessage += (_, args) =>
{
jslogger.Debug(args.Message + " (" + args.Source + ":" + args.Line + ")");
};
Controls.Add(Browser);
}
private void MainForm_Load(object sender, System.EventArgs e)
{
try
{
int.TryParse(VRCXStorage.Instance.Get("VRCX_LocationX"), out LastLocationX);
int.TryParse(VRCXStorage.Instance.Get("VRCX_LocationY"), out LastLocationY);
int.TryParse(VRCXStorage.Instance.Get("VRCX_SizeWidth"), out LastSizeWidth);
int.TryParse(VRCXStorage.Instance.Get("VRCX_SizeHeight"), out LastSizeHeight);
var location = new Point(LastLocationX, LastLocationY);
var size = new Size(LastSizeWidth, LastSizeHeight);
var screen = Screen.FromPoint(location);
if (screen.Bounds.Contains(location.X, location.Y))
{
Location = location;
}
Size = new Size(1920, 1080);
if (size.Width > 0 && size.Height > 0)
{
Size = size;
}
}
catch
{
}
try
{
var state = WindowState;
if (int.TryParse(VRCXStorage.Instance.Get("VRCX_WindowState"), out int v))
{
state = (FormWindowState)v;
}
if (state == FormWindowState.Minimized)
{
state = FormWindowState.Normal;
}
if ("true".Equals(VRCXStorage.Instance.Get("VRCX_StartAsMinimizedState")))
{
state = FormWindowState.Minimized;
}
if ("true".Equals(VRCXStorage.Instance.Get("VRCX_StartAsMinimizedState")) &&
"true".Equals(VRCXStorage.Instance.Get("VRCX_CloseToTray")))
{
BeginInvoke(new MethodInvoker(Hide));
}
else
{
WindowState = state;
}
}
catch
{
}
// 가끔 화면 위치가 안맞음.. 이걸로 해결 될지는 모르겠음
Browser.Invalidate();
}
private void MainForm_Resize(object sender, System.EventArgs e)
{
if (WindowState != FormWindowState.Normal)
{
return;
}
LastSizeWidth = Size.Width;
LastSizeHeight = Size.Height;
}
private void MainForm_Move(object sender, System.EventArgs e)
{
if (WindowState != FormWindowState.Normal)
{
return;
}
LastLocationX = Location.X;
LastLocationY = Location.Y;
}
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing &&
"true".Equals(VRCXStorage.Instance.Get("VRCX_CloseToTray")))
{
e.Cancel = true;
Hide();
}
}
private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
{
VRCXStorage.Instance.Set("VRCX_LocationX", LastLocationX.ToString());
VRCXStorage.Instance.Set("VRCX_LocationY", LastLocationY.ToString());
VRCXStorage.Instance.Set("VRCX_SizeWidth", LastSizeWidth.ToString());
VRCXStorage.Instance.Set("VRCX_SizeHeight", LastSizeHeight.ToString());
VRCXStorage.Instance.Set("VRCX_WindowState", ((int)WindowState).ToString());
}
private void TrayIcon_MouseClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
Focus_Window();
}
}
private void TrayMenu_Open_Click(object sender, System.EventArgs e)
{
Focus_Window();
}
public void Focus_Window()
{
if (WindowState == FormWindowState.Minimized)
{
WindowState = FormWindowState.Normal;
}
Show();
// Focus();
Activate();
}
private void TrayMenu_DevTools_Click(object sender, System.EventArgs e)
{
Instance.Browser.ShowDevTools();
}
private void TrayMenu_Quit_Click(object sender, System.EventArgs e)
{
Application.Exit();
}
}
}
+126
View File
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="TrayMenu.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="TrayIcon.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>124, 17</value>
</metadata>
</root>
File diff suppressed because it is too large Load Diff
+201
View File
@@ -0,0 +1,201 @@
// Copyright(c) 2019-2022 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 CefSharp;
using CefSharp.Enums;
using CefSharp.OffScreen;
using CefSharp.Structs;
using SharpDX.Direct3D11;
using System;
using System.Runtime.InteropServices;
using System.Threading;
namespace VRCX
{
public class OffScreenBrowser : ChromiumWebBrowser, IRenderHandler
{
private readonly ReaderWriterLockSlim _paintBufferLock;
private GCHandle _paintBuffer;
private int _width;
private int _height;
public OffScreenBrowser(string address, int width, int height)
: base(
address,
new BrowserSettings()
{
DefaultEncoding = "UTF-8"
}
)
{
_paintBufferLock = new ReaderWriterLockSlim();
Size = new System.Drawing.Size(width, height);
RenderHandler = this;
Util.ApplyJavascriptBindings(JavascriptObjectRepository);
}
public new void Dispose()
{
RenderHandler = null;
base.Dispose();
_paintBufferLock.EnterWriteLock();
try
{
if (_paintBuffer.IsAllocated == true)
{
_paintBuffer.Free();
}
}
finally
{
_paintBufferLock.ExitWriteLock();
}
_paintBufferLock.Dispose();
}
public void RenderToTexture(Texture2D texture)
{
_paintBufferLock.EnterReadLock();
try
{
if (_width > 0 &&
_height > 0)
{
var context = texture.Device.ImmediateContext;
var dataBox = context.MapSubresource(
texture,
0,
MapMode.WriteDiscard,
MapFlags.None
);
if (dataBox.IsEmpty == false)
{
var sourcePtr = _paintBuffer.AddrOfPinnedObject();
var destinationPtr = dataBox.DataPointer;
var pitch = _width * 4;
var rowPitch = dataBox.RowPitch;
if (pitch == rowPitch)
{
WinApi.CopyMemory(
destinationPtr,
sourcePtr,
(uint)(_width * _height * 4)
);
}
else
{
for (var y = _height; y > 0; --y)
{
WinApi.CopyMemory(
destinationPtr,
sourcePtr,
(uint)pitch
);
sourcePtr += pitch;
destinationPtr += rowPitch;
}
}
}
context.UnmapSubresource(texture, 0);
}
}
finally
{
_paintBufferLock.ExitReadLock();
}
}
ScreenInfo? IRenderHandler.GetScreenInfo()
{
return null;
}
bool IRenderHandler.GetScreenPoint(int viewX, int viewY, out int screenX, out int screenY)
{
screenX = viewX;
screenY = viewY;
return false;
}
Rect IRenderHandler.GetViewRect()
{
return new Rect(0, 0, Size.Width, Size.Height);
}
void IRenderHandler.OnAcceleratedPaint(PaintElementType type, Rect dirtyRect, IntPtr sharedHandle)
{
// NOT USED
}
void IRenderHandler.OnCursorChange(IntPtr cursor, CursorType type, CursorInfo customCursorInfo)
{
}
void IRenderHandler.OnImeCompositionRangeChanged(Range selectedRange, Rect[] characterBounds)
{
}
void IRenderHandler.OnPaint(PaintElementType type, Rect dirtyRect, IntPtr buffer, int width, int height)
{
if (type == PaintElementType.View)
{
_paintBufferLock.EnterWriteLock();
try
{
if (_width != width ||
_height != height)
{
_width = width;
_height = height;
if (_paintBuffer.IsAllocated == true)
{
_paintBuffer.Free();
}
_paintBuffer = GCHandle.Alloc(
new byte[_width * _height * 4],
GCHandleType.Pinned
);
}
WinApi.CopyMemory(
_paintBuffer.AddrOfPinnedObject(),
buffer,
(uint)(width * height * 4)
);
}
finally
{
_paintBufferLock.ExitWriteLock();
}
}
}
void IRenderHandler.OnPopupShow(bool show)
{
}
void IRenderHandler.OnPopupSize(Rect rect)
{
}
void IRenderHandler.OnVirtualKeyboardRequested(IBrowser browser, TextInputMode inputMode)
{
}
bool IRenderHandler.StartDragging(IDragData dragData, DragOperationsMask mask, int x, int y)
{
return false;
}
void IRenderHandler.UpdateDragCursor(DragOperationsMask operation)
{
}
}
}
+713
View File
@@ -0,0 +1,713 @@
// Copyright(c) 2019-2022 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;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using CefSharp;
using SharpDX;
using SharpDX.Direct3D;
using SharpDX.Direct3D11;
using SharpDX.DXGI;
using Valve.VR;
using Device = SharpDX.Direct3D11.Device;
namespace VRCX
{
public class VRCXVR
{
public static VRCXVR Instance;
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private static readonly float[] _rotation = { 0f, 0f, 0f };
private static readonly float[] _translation = { 0f, 0f, 0f };
private static readonly float[] _translationLeft = { -7f / 100f, -5f / 100f, 6f / 100f };
private static readonly float[] _translationRight = { 7f / 100f, -5f / 100f, 6f / 100f };
private static readonly float[] _rotationLeft = { 90f * (float)(Math.PI / 180f), 90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f) };
private static readonly float[] _rotationRight = { -90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f), -90f * (float)(Math.PI / 180f) };
public static OffScreenBrowser _browser1;
public static OffScreenBrowser _browser2;
private readonly List<string[]> _deviceList;
private readonly ReaderWriterLockSlim _deviceListLock;
private bool _active;
private Device _device;
private bool _hmdOverlayActive;
private bool _menuButton;
private int _overlayHand;
private Texture2D _texture1;
private Texture2D _texture2;
private Thread _thread;
private bool _wristOverlayActive;
static VRCXVR()
{
Instance = new VRCXVR();
}
public VRCXVR()
{
_deviceListLock = new ReaderWriterLockSlim();
_deviceList = new List<string[]>();
_thread = new Thread(ThreadLoop)
{
IsBackground = true
};
}
// NOTE
// 메모리 릭 때문에 미리 생성해놓고 계속 사용함
internal void Init()
{
_thread.Start();
}
internal void Exit()
{
var thread = _thread;
_thread = null;
thread.Interrupt();
thread.Join();
}
public void Restart()
{
Exit();
Instance = new VRCXVR();
Instance.Init();
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
}
private void ThreadLoop()
{
var active = false;
var e = new VREvent_t();
var nextInit = DateTime.MinValue;
var nextDeviceUpdate = DateTime.MinValue;
var nextOverlay = DateTime.MinValue;
var overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
var overlayVisible1 = false;
var overlayVisible2 = false;
var dashboardHandle = 0UL;
var overlayHandle1 = 0UL;
var overlayHandle2 = 0UL;
// REMOVE THIS
// nextOverlay = DateTime.MaxValue;
// https://stackoverflow.com/questions/38312597/how-to-choose-a-specific-graphics-device-in-sharpdx-directx-11/38596725#38596725
Factory f = new Factory1();
var a = f.GetAdapter(1);
var flags = DeviceCreationFlags.BgraSupport;
_device = Program.GPUFix ? new Device(a, flags) : new Device(DriverType.Hardware, DeviceCreationFlags.SingleThreaded | DeviceCreationFlags.BgraSupport);
_texture1 = new Texture2D(
_device,
new Texture2DDescription
{
Width = 512,
Height = 512,
MipLevels = 1,
ArraySize = 1,
Format = Format.B8G8R8A8_UNorm,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Dynamic,
BindFlags = BindFlags.ShaderResource,
CpuAccessFlags = CpuAccessFlags.Write
}
);
_texture2 = new Texture2D(
_device,
new Texture2DDescription
{
Width = 1024,
Height = 1024,
MipLevels = 1,
ArraySize = 1,
Format = Format.B8G8R8A8_UNorm,
SampleDescription = new SampleDescription(1, 0),
Usage = ResourceUsage.Dynamic,
BindFlags = BindFlags.ShaderResource,
CpuAccessFlags = CpuAccessFlags.Write
}
);
_browser1 = new OffScreenBrowser(
"file://vrcx/vr.html?1",
512,
512
);
_browser2 = new OffScreenBrowser(
"file://vrcx/vr.html?2",
1024,
1024
);
while (_thread != null)
{
if (_wristOverlayActive)
_browser1.RenderToTexture(_texture1);
if (_hmdOverlayActive)
_browser2.RenderToTexture(_texture2);
try
{
Thread.Sleep(32);
}
catch (ThreadInterruptedException)
{
}
if (_active)
{
var system = OpenVR.System;
if (system == null)
{
if (DateTime.UtcNow.CompareTo(nextInit) <= 0)
{
continue;
}
var _err = EVRInitError.None;
system = OpenVR.Init(ref _err, EVRApplicationType.VRApplication_Background);
nextInit = DateTime.UtcNow.AddSeconds(5);
if (system == null)
{
continue;
}
active = true;
}
while (system.PollNextEvent(ref e, (uint)Marshal.SizeOf(e)))
{
var type = (EVREventType)e.eventType;
if (type == EVREventType.VREvent_Quit)
{
active = false;
OpenVR.Shutdown();
nextInit = DateTime.UtcNow.AddSeconds(10);
system = null;
break;
}
}
if (system != null)
{
if (DateTime.UtcNow.CompareTo(nextDeviceUpdate) >= 0)
{
overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
UpdateDevices(system, ref overlayIndex);
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
{
nextOverlay = DateTime.UtcNow.AddSeconds(10);
}
nextDeviceUpdate = DateTime.UtcNow.AddSeconds(0.1);
}
var overlay = OpenVR.Overlay;
if (overlay != null)
{
var dashboardVisible = overlay.IsDashboardVisible();
var err = ProcessDashboard(overlay, ref dashboardHandle, dashboardVisible);
if (err != EVROverlayError.None &&
dashboardHandle != 0)
{
overlay.DestroyOverlay(dashboardHandle);
dashboardHandle = 0;
logger.Error(err);
}
err = ProcessOverlay1(overlay, ref overlayHandle1, ref overlayVisible1, dashboardVisible, overlayIndex, nextOverlay);
if (err != EVROverlayError.None &&
overlayHandle1 != 0)
{
overlay.DestroyOverlay(overlayHandle1);
overlayHandle1 = 0;
logger.Error(err);
}
err = ProcessOverlay2(overlay, ref overlayHandle2, ref overlayVisible2, dashboardVisible);
if (err != EVROverlayError.None &&
overlayHandle2 != 0)
{
overlay.DestroyOverlay(overlayHandle2);
overlayHandle2 = 0;
logger.Error(err);
}
}
}
}
else if (active)
{
active = false;
OpenVR.Shutdown();
_deviceListLock.EnterWriteLock();
try
{
_deviceList.Clear();
}
finally
{
_deviceListLock.ExitWriteLock();
}
}
}
_browser2.Dispose();
_browser1.Dispose();
_texture2.Dispose();
_texture1.Dispose();
_device.Dispose();
}
public void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
{
_active = active;
_hmdOverlayActive = hmdOverlay;
_wristOverlayActive = wristOverlay;
_menuButton = menuButton;
_overlayHand = overlayHand;
}
public void Refresh()
{
_browser1.Reload();
_browser2.Reload();
}
public string[][] GetDevices()
{
_deviceListLock.EnterReadLock();
try
{
return _deviceList.ToArray();
}
finally
{
_deviceListLock.ExitReadLock();
}
}
internal void UpdateDevices(CVRSystem system, ref uint overlayIndex)
{
_deviceListLock.EnterWriteLock();
try
{
_deviceList.Clear();
}
finally
{
_deviceListLock.ExitWriteLock();
}
var sb = new StringBuilder(256);
var state = new VRControllerState_t();
var poses = new TrackedDevicePose_t[OpenVR.k_unMaxTrackedDeviceCount];
system.GetDeviceToAbsoluteTrackingPose(ETrackingUniverseOrigin.TrackingUniverseStanding, 0, poses);
for (var i = 0u; i < OpenVR.k_unMaxTrackedDeviceCount; ++i)
{
var devClass = system.GetTrackedDeviceClass(i);
if (devClass == ETrackedDeviceClass.Controller ||
devClass == ETrackedDeviceClass.GenericTracker ||
devClass == ETrackedDeviceClass.TrackingReference)
{
var err = ETrackedPropertyError.TrackedProp_Success;
var batteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref err);
if (err != ETrackedPropertyError.TrackedProp_Success)
{
batteryPercentage = 1f;
}
err = ETrackedPropertyError.TrackedProp_Success;
var isCharging = system.GetBoolTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceIsCharging_Bool, ref err);
if (err != ETrackedPropertyError.TrackedProp_Success)
{
isCharging = false;
}
sb.Clear();
system.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_TrackingSystemName_String, sb, (uint)sb.Capacity, ref err);
var isOculus = sb.ToString().IndexOf("oculus", StringComparison.OrdinalIgnoreCase) >= 0;
// Oculus : B/Y, Bit 1, Mask 2
// Oculus : A/X, Bit 7, Mask 128
// Vive : Menu, Bit 1, Mask 2,
// Vive : Grip, Bit 2, Mask 4
var role = system.GetControllerRoleForTrackedDeviceIndex(i);
if (role == ETrackedControllerRole.LeftHand || role == ETrackedControllerRole.RightHand)
{
if (_overlayHand == 0 ||
(_overlayHand == 1 && role == ETrackedControllerRole.LeftHand) ||
(_overlayHand == 2 && role == ETrackedControllerRole.RightHand))
{
if (system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state)) &&
(state.ulButtonPressed & (_menuButton ? 2u : isOculus ? 128u : 4u)) != 0)
{
if (role == ETrackedControllerRole.LeftHand)
{
Array.Copy(_translationLeft, _translation, 3);
Array.Copy(_rotationLeft, _rotation, 3);
}
else
{
Array.Copy(_translationRight, _translation, 3);
Array.Copy(_rotationRight, _rotation, 3);
}
overlayIndex = i;
}
}
}
var type = string.Empty;
if (devClass == ETrackedDeviceClass.Controller)
{
if (role == ETrackedControllerRole.LeftHand)
{
type = "leftController";
}
else if (role == ETrackedControllerRole.RightHand)
{
type = "rightController";
}
else
{
type = "controller";
}
}
else if (devClass == ETrackedDeviceClass.GenericTracker)
{
type = "tracker";
}
else if (devClass == ETrackedDeviceClass.TrackingReference)
{
type = "base";
}
var item = new[]
{
type,
system.IsTrackedDeviceConnected(i)
? "connected"
: "disconnected",
isCharging
? "charging"
: "discharging",
(batteryPercentage * 100).ToString(),
poses[i].eTrackingResult.ToString()
};
_deviceListLock.EnterWriteLock();
try
{
_deviceList.Add(item);
}
finally
{
_deviceListLock.ExitWriteLock();
}
}
}
}
internal EVROverlayError ProcessDashboard(CVROverlay overlay, ref ulong dashboardHandle, bool dashboardVisible)
{
var err = EVROverlayError.None;
if (dashboardHandle == 0)
{
err = overlay.FindOverlay("VRCX", ref dashboardHandle);
if (err != EVROverlayError.None)
{
if (err != EVROverlayError.UnknownOverlay)
{
return err;
}
ulong thumbnailHandle = 0;
err = overlay.CreateDashboardOverlay("VRCX", "VRCX", ref dashboardHandle, ref thumbnailHandle);
if (err != EVROverlayError.None)
{
return err;
}
var iconPath = Path.Combine(Program.BaseDirectory, "VRCX.png");
err = overlay.SetOverlayFromFile(thumbnailHandle, iconPath);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayWidthInMeters(dashboardHandle, 1.5f);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayInputMethod(dashboardHandle, VROverlayInputMethod.Mouse);
if (err != EVROverlayError.None)
{
return err;
}
}
}
var e = new VREvent_t();
while (overlay.PollNextOverlayEvent(dashboardHandle, ref e, (uint)Marshal.SizeOf(e)))
{
var type = (EVREventType)e.eventType;
if (type == EVREventType.VREvent_MouseMove)
{
var m = e.data.mouse;
var s = _browser1.Size;
_browser1.GetBrowserHost().SendMouseMoveEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), false, CefEventFlags.None);
}
else if (type == EVREventType.VREvent_MouseButtonDown)
{
var m = e.data.mouse;
var s = _browser1.Size;
_browser1.GetBrowserHost().SendMouseClickEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), MouseButtonType.Left, false, 1, CefEventFlags.LeftMouseButton);
}
else if (type == EVREventType.VREvent_MouseButtonUp)
{
var m = e.data.mouse;
var s = _browser1.Size;
_browser1.GetBrowserHost().SendMouseClickEvent((int)(m.x * s.Width), s.Height - (int)(m.y * s.Height), MouseButtonType.Left, true, 1, CefEventFlags.None);
}
}
if (dashboardVisible)
{
var texture = new Texture_t
{
handle = _texture1.NativePointer
};
err = overlay.SetOverlayTexture(dashboardHandle, ref texture);
if (err != EVROverlayError.None)
{
return err;
}
}
return err;
}
internal EVROverlayError ProcessOverlay1(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible, uint overlayIndex, DateTime nextOverlay)
{
var err = EVROverlayError.None;
if (overlayHandle == 0)
{
err = overlay.FindOverlay("VRCX1", ref overlayHandle);
if (err != EVROverlayError.None)
{
if (err != EVROverlayError.UnknownOverlay)
{
return err;
}
overlayVisible = false;
err = overlay.CreateOverlay("VRCX1", "VRCX1", ref overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayAlpha(overlayHandle, 0.9f);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayWidthInMeters(overlayHandle, 1f);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayInputMethod(overlayHandle, VROverlayInputMethod.None);
if (err != EVROverlayError.None)
{
return err;
}
}
}
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
{
// http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices
// Scaling-Rotation-Translation
var m = Matrix.Scaling(0.25f);
m *= Matrix.RotationX(_rotation[0]);
m *= Matrix.RotationY(_rotation[1]);
m *= Matrix.RotationZ(_rotation[2]);
m *= Matrix.Translation(_translation[0], _translation[1], _translation[2]);
var hm34 = new HmdMatrix34_t
{
m0 = m.M11,
m1 = m.M21,
m2 = m.M31,
m3 = m.M41,
m4 = m.M12,
m5 = m.M22,
m6 = m.M32,
m7 = m.M42,
m8 = m.M13,
m9 = m.M23,
m10 = m.M33,
m11 = m.M43
};
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, overlayIndex, ref hm34);
if (err != EVROverlayError.None)
{
return err;
}
}
if (!dashboardVisible &&
DateTime.UtcNow.CompareTo(nextOverlay) <= 0)
{
var texture = new Texture_t
{
handle = _texture1.NativePointer
};
err = overlay.SetOverlayTexture(overlayHandle, ref texture);
if (err != EVROverlayError.None)
{
return err;
}
if (!overlayVisible)
{
err = overlay.ShowOverlay(overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
overlayVisible = true;
}
}
else if (overlayVisible)
{
err = overlay.HideOverlay(overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
overlayVisible = false;
}
return err;
}
internal EVROverlayError ProcessOverlay2(CVROverlay overlay, ref ulong overlayHandle, ref bool overlayVisible, bool dashboardVisible)
{
var err = EVROverlayError.None;
if (overlayHandle == 0)
{
err = overlay.FindOverlay("VRCX2", ref overlayHandle);
if (err != EVROverlayError.None)
{
if (err != EVROverlayError.UnknownOverlay)
{
return err;
}
overlayVisible = false;
err = overlay.CreateOverlay("VRCX2", "VRCX2", ref overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayAlpha(overlayHandle, 0.9f);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayWidthInMeters(overlayHandle, 1f);
if (err != EVROverlayError.None)
{
return err;
}
err = overlay.SetOverlayInputMethod(overlayHandle, VROverlayInputMethod.None);
if (err != EVROverlayError.None)
{
return err;
}
var m = Matrix.Scaling(1f);
m *= Matrix.Translation(0, -0.3f, -1.5f);
var hm34 = new HmdMatrix34_t
{
m0 = m.M11,
m1 = m.M21,
m2 = m.M31,
m3 = m.M41,
m4 = m.M12,
m5 = m.M22,
m6 = m.M32,
m7 = m.M42,
m8 = m.M13,
m9 = m.M23,
m10 = m.M33,
m11 = m.M43
};
err = overlay.SetOverlayTransformTrackedDeviceRelative(overlayHandle, OpenVR.k_unTrackedDeviceIndex_Hmd, ref hm34);
if (err != EVROverlayError.None)
{
return err;
}
}
}
if (!dashboardVisible)
{
var texture = new Texture_t
{
handle = _texture2.NativePointer
};
err = overlay.SetOverlayTexture(overlayHandle, ref texture);
if (err != EVROverlayError.None)
{
return err;
}
if (!overlayVisible)
{
err = overlay.ShowOverlay(overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
overlayVisible = true;
}
}
else if (overlayVisible)
{
err = overlay.HideOverlay(overlayHandle);
if (err != EVROverlayError.None)
{
return err;
}
overlayVisible = false;
}
return err;
}
}
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright(c) 2019-2022 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>.
namespace VRCX
{
partial class VRForm
{
/// <summary>
/// 필수 디자이너 변수입니다.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// 사용 중인 모든 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 삭제해야 하면 true이고, 그렇지 않으면 false입니다.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form
/// <summary>
/// 디자이너 지원에 필요한 메서드입니다.
/// 이 메서드의 내용을 코드 편집기로 수정하지 마세요.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.timer = new System.Windows.Forms.Timer(this.components);
this.panel1 = new System.Windows.Forms.Panel();
this.panel2 = new System.Windows.Forms.Panel();
this.button_refresh = new System.Windows.Forms.Button();
this.button_devtools = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// panel1
//
this.panel1.Location = new System.Drawing.Point(0, 0);
this.panel1.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(731, 768);
this.panel1.TabIndex = 0;
//
// panel2
//
this.panel2.Location = new System.Drawing.Point(740, 0);
this.panel2.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.panel2.Name = "panel2";
this.panel2.Size = new System.Drawing.Size(731, 768);
this.panel2.TabIndex = 1;
//
// button_refresh
//
this.button_refresh.Location = new System.Drawing.Point(17, 777);
this.button_refresh.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.button_refresh.Name = "button_refresh";
this.button_refresh.Size = new System.Drawing.Size(107, 34);
this.button_refresh.TabIndex = 27;
this.button_refresh.Text = "Refresh";
this.button_refresh.UseVisualStyleBackColor = true;
this.button_refresh.Click += new System.EventHandler(this.button_refresh_Click);
//
// button_devtools
//
this.button_devtools.Location = new System.Drawing.Point(133, 777);
this.button_devtools.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.button_devtools.Name = "button_devtools";
this.button_devtools.Size = new System.Drawing.Size(107, 34);
this.button_devtools.TabIndex = 27;
this.button_devtools.Text = "DevTools";
this.button_devtools.UseVisualStyleBackColor = true;
this.button_devtools.Click += new System.EventHandler(this.button_devtools_Click);
//
// VRForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(144F, 144F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
this.ClientSize = new System.Drawing.Size(1483, 830);
this.Controls.Add(this.button_devtools);
this.Controls.Add(this.button_refresh);
this.Controls.Add(this.panel2);
this.Controls.Add(this.panel1);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.Margin = new System.Windows.Forms.Padding(4, 4, 4, 4);
this.MaximizeBox = false;
this.Name = "VRForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "VR";
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Timer timer;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Panel panel2;
private System.Windows.Forms.Button button_refresh;
private System.Windows.Forms.Button button_devtools;
}
}
+68
View File
@@ -0,0 +1,68 @@
// Copyright(c) 2019 pypy. 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.IO;
using System.Windows.Forms;
using CefSharp;
using CefSharp.WinForms;
namespace VRCX
{
public partial class VRForm : WinformBase
{
public static VRForm Instance;
private ChromiumWebBrowser _browser1;
private ChromiumWebBrowser _browser2;
public VRForm()
{
Instance = this;
InitializeComponent();
_browser1 = new ChromiumWebBrowser(
Path.Combine(Program.BaseDirectory, "html/vr.html?1")
)
{
DragHandler = new CefNoopDragHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
_browser2 = new ChromiumWebBrowser(
Path.Combine(Program.BaseDirectory, "html/vr.html?2")
)
{
DragHandler = new CefNoopDragHandler(),
BrowserSettings =
{
DefaultEncoding = "UTF-8",
},
Dock = DockStyle.Fill
};
Util.ApplyJavascriptBindings(_browser1.JavascriptObjectRepository);
Util.ApplyJavascriptBindings(_browser2.JavascriptObjectRepository);
panel1.Controls.Add(_browser1);
panel2.Controls.Add(_browser2);
}
private void button_refresh_Click(object sender, System.EventArgs e)
{
_browser1.ExecuteScriptAsync("location.reload()");
_browser2.ExecuteScriptAsync("location.reload()");
VRCXVR.Instance.Refresh();
}
private void button_devtools_Click(object sender, System.EventArgs e)
{
_browser1.ShowDevTools();
_browser2.ShowDevTools();
}
}
}
+123
View File
@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="timer.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
</root>
+711
View File
@@ -0,0 +1,711 @@
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 static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private string lastError = null;
private bool debugWorld = false;
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()
{
// 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 != null && worldId != worldOverride)
{
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride);
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 != null && worldId != worldOverride)
{
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride);
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 != null && worldId != worldOverride)
{
var allowed = worldDB.GetWorldAllowExternalRead(worldOverride);
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);
// 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 (String.IsNullOrEmpty(request.Value))
{
logger.Warn("World {0} tried to store data under key {1} with no value provided", worldId, request.Key);
this.lastError = "`value` is missing or 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()
{
listener?.Stop();
listener?.Close();
worldDB?.Close();
}
}
}
+55
View File
@@ -0,0 +1,55 @@
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;
}
}
+322
View File
@@ -0,0 +1,322 @@
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();
}
}
}
+204
View File
@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Timers;
namespace VRCX
{
// I don't think this applies to our use case, but I'm leaving it here for reference.
// https://stackoverflow.com/questions/2519673/process-hasexited-returns-true-even-though-process-is-running
// "When a process is started, it is assigned a PID. If the User is then prompted with the User Account Control dialog and selects 'Yes', the process is re-started and assigned a new PID."
// There's no docs for this, but Process.HasExited also seems to be checked every time the property is accessed, so it's not cached. Which means Process.Refresh() is not needed for our use case.
/// <summary>
/// A class that monitors given processes and raises events when they are started or exited.
/// Intended to be used to monitor VRChat and VRChat-related processes.
/// </summary>
internal class ProcessMonitor
{
private readonly Dictionary<string, MonitoredProcess> monitoredProcesses;
private readonly Timer monitorProcessTimer;
static ProcessMonitor()
{
Instance = new ProcessMonitor();
}
public ProcessMonitor()
{
monitoredProcesses = new Dictionary<string, MonitoredProcess>();
monitorProcessTimer = new Timer();
monitorProcessTimer.Interval = 1000;
monitorProcessTimer.Elapsed += MonitorProcessTimer_Elapsed;
}
public static ProcessMonitor Instance { get; private set; }
/// <summary>
/// Raised when a monitored process is started.
/// </summary>
public event Action<MonitoredProcess> ProcessStarted;
/// <summary>
/// Raised when a monitored process is exited.
/// </summary>
public event Action<MonitoredProcess> ProcessExited;
public void Init()
{
AddProcess("vrchat");
AddProcess("vrserver");
monitorProcessTimer.Start();
}
public void Exit()
{
monitorProcessTimer.Stop();
monitoredProcesses.Values.ToList().ForEach(x => x.ProcessExited());
}
private void MonitorProcessTimer_Elapsed(object sender, ElapsedEventArgs e)
{
var processesNeedingUpdate = new List<MonitoredProcess>();
// Check if any of the monitored processes have been opened or closed.
foreach (var keyValuePair in monitoredProcesses)
{
var monitoredProcess = keyValuePair.Value;
if (monitoredProcess.IsRunning)
{
if (monitoredProcess.Process == null || WinApi.HasProcessExited(monitoredProcess.Process.Id))
{
monitoredProcess.ProcessExited();
ProcessExited?.Invoke(monitoredProcess);
}
}
else
{
processesNeedingUpdate.Add(monitoredProcess);
}
}
// We do it this way so we're not constantly polling for processes if we don't actually need to (aka, all processes are already accounted for).
if (processesNeedingUpdate.Count == 0)
return;
var processes = Process.GetProcesses();
foreach (var monitoredProcess in processesNeedingUpdate)
{
var process = processes.FirstOrDefault(p => string.Equals(p.ProcessName, monitoredProcess.ProcessName, StringComparison.OrdinalIgnoreCase));
if (process == null)
continue;
monitoredProcess.ProcessStarted(process);
ProcessStarted?.Invoke(monitoredProcess);
}
}
/// <summary>
/// Checks if a process if currently being monitored and if it is running.
/// </summary>
/// <param name="processName">The name of the process to check for.</param>
/// <param name="ensureCheck">If true, will manually check if the given process is running should the the monitored process not be initialized yet.</param>
/// <returns>Whether the given process is monitored and currently running.</returns>
public bool IsProcessRunning(string processName, bool ensureCheck = false)
{
processName = processName.ToLower();
if (!monitoredProcesses.TryGetValue(processName, out var process))
return false;
if (ensureCheck && process.Process == null)
return Process.GetProcessesByName(processName).FirstOrDefault() != null;
return process.IsRunning;
}
/// <summary>
/// Adds a process to be monitored.
/// </summary>
/// <param name="process"></param>
public void AddProcess(Process process)
{
var processName = process.ProcessName.ToLower();
if (monitoredProcesses.ContainsKey(processName))
return;
monitoredProcesses.Add(processName, new MonitoredProcess(process));
}
/// <summary>
/// Adds a process to be monitored.
/// </summary>
/// <param name="processName"></param>
public void AddProcess(string processName)
{
processName = processName.ToLower();
if (monitoredProcesses.ContainsKey(processName))
return;
monitoredProcesses.Add(processName, new MonitoredProcess(processName));
}
/// <summary>
/// Removes a process from being monitored.
/// </summary>
/// <param name="processName"></param>
public void RemoveProcess(string processName)
{
processName = processName.ToLower();
monitoredProcesses.Remove(processName);
}
}
internal class MonitoredProcess
{
public MonitoredProcess(Process process)
{
Process = process;
ProcessName = process.ProcessName.ToLower();
if (!WinApi.HasProcessExited(process.Id))
IsRunning = true;
}
public MonitoredProcess(string processName)
{
ProcessName = processName;
IsRunning = false;
}
public Process Process { get; private set; }
public string ProcessName { get; private set; }
public bool IsRunning { get; private set; }
public DateTime LastExitTime { get; private set; }
public bool HasName(string processName)
{
return ProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase);
}
public void ProcessExited()
{
IsRunning = false;
Process?.Dispose();
Process = null;
LastExitTime = DateTime.Now;
}
public void ProcessStarted(Process process)
{
// check last exit time to prevent status flapping
if (LastExitTime != DateTime.MinValue && DateTime.Now - LastExitTime < TimeSpan.FromSeconds(5))
return;
Process = process;
ProcessName = process.ProcessName.ToLower();
IsRunning = true;
}
}
}
+183
View File
@@ -0,0 +1,183 @@
// Copyright(c) 2019-2022 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 NLog;
using NLog.Targets;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace VRCX
{
public static class Program
{
public static string BaseDirectory { get; private set; }
public static readonly string AppDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX");
public static string ConfigLocation;
public static string Version { get; private set; }
public static bool LaunchDebug;
public static bool GPUFix;
private static readonly NLog.Logger logger = NLog.LogManager.GetLogger("VRCX");
static Program()
{
BaseDirectory = AppDomain.CurrentDomain.BaseDirectory;
ConfigLocation = Path.Combine(Program.AppDataDirectory, "VRCX.sqlite3");
if (!Directory.Exists(AppDataDirectory))
{
Directory.CreateDirectory(AppDataDirectory);
// Migrate config to AppData
if (File.Exists(Path.Combine(BaseDirectory, "VRCX.json")))
{
File.Move(Path.Combine(BaseDirectory, "VRCX.json"), Path.Combine(AppDataDirectory, "VRCX.json"));
File.Copy(Path.Combine(AppDataDirectory, "VRCX.json"), Path.Combine(AppDataDirectory, "VRCX-backup.json"));
}
if (File.Exists(Path.Combine(BaseDirectory, "VRCX.sqlite3")))
{
File.Move(Path.Combine(BaseDirectory, "VRCX.sqlite3"), Path.Combine(AppDataDirectory, "VRCX.sqlite3"));
File.Copy(Path.Combine(AppDataDirectory, "VRCX.sqlite3"), Path.Combine(AppDataDirectory, "VRCX-backup.sqlite3"));
}
}
// Migrate cache to userdata for Cef 115 update
var oldCachePath = Path.Combine(AppDataDirectory, "cache");
if (Directory.Exists(oldCachePath))
{
var newCachePath = Path.Combine(AppDataDirectory, "userdata", "cache");
if (Directory.Exists(newCachePath))
Directory.Delete(newCachePath, true);
Directory.Move(oldCachePath, newCachePath);
}
}
private static void ConfigureLogger()
{
NLog.LogManager.Setup().LoadConfiguration(builder =>
{
var fileTarget = new FileTarget("fileTarget")
{
FileName = Path.Combine(AppDataDirectory, "logs", "VRCX.log"),
//Layout = "${longdate} [${level:uppercase=true}] ${logger} - ${message} ${exception:format=tostring}",
// 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.Combine(AppDataDirectory, "logs", "VRCX.{#}.log"),
ArchiveNumbering = ArchiveNumberingMode.DateAndSequence,
ArchiveEvery = FileArchivePeriod.Day,
MaxArchiveFiles = 4,
MaxArchiveDays = 7,
ArchiveAboveSize = 10000000,
ArchiveOldFileOnStartup = true,
ConcurrentWrites = true,
KeepFileOpen = true,
AutoFlush = true,
Encoding = System.Text.Encoding.UTF8
};
if (Program.LaunchDebug)
{
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
}
else
{
#if DEBUG
// Archive maximum of 3 files 10MB each, kept for a maximum of 7 days
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
#else
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
#endif
}
});
}
[STAThread]
private static void Main()
{
ConfigureLogger();
try
{
Run();
}
catch (Exception e)
{
logger.Fatal(e, "Unhandled Exception, program dying");
MessageBox.Show(e.ToString(), "PLEASE REPORT IN https://vrcx.pypy.moe/discord", MessageBoxButtons.OK, MessageBoxIcon.Error);
Environment.Exit(0);
}
}
private static void GetVersion()
{
var buildName = "VRCX";
try
{
Version = $"{buildName} {File.ReadAllText(Path.Combine(BaseDirectory, "Version"))}";
}
catch (Exception)
{
Version = $"{buildName} Build";
}
}
private static void Run()
{
Update.Check();
StartupArgs.ArgsCheck();
GetVersion();
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
logger.Info("{0} Starting...", Version);
// 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();
SQLiteLegacy.Instance.Init();
VRCXStorage.Load();
LoadFromConfig();
CpuMonitor.Instance.Init();
Discord.Instance.Init();
WebApi.Instance.Init();
LogWatcher.Instance.Init();
AutoAppLaunchManager.Instance.Init();
CefService.Instance.Init();
IPCServer.Instance.Init();
VRCXVR.Instance.Init();
Application.Run(new MainForm());
logger.Info("{0} Exiting...", Version);
WebApi.Instance.SaveCookies();
VRCXVR.Instance.Exit();
CefService.Instance.Exit();
AutoAppLaunchManager.Instance.Exit();
LogWatcher.Instance.Exit();
WebApi.Instance.Exit();
worldDBServer.Stop();
Discord.Instance.Exit();
CpuMonitor.Instance.Exit();
VRCXStorage.Save();
SQLiteLegacy.Instance.Exit();
ProcessMonitor.Instance.Exit();
}
/// <summary>
/// Sets GPUFix to true if it is not already set and the VRCX_GPUFix key in the database is true.
/// </summary>
private static void LoadFromConfig()
{
if (!GPUFix)
GPUFix = VRCXStorage.Instance.Get("VRCX_GPUFix") == "true";
}
}
}
+4956
View File
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
using CefSharp;
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.IO;
using System.Threading;
namespace VRCX
{
public class SQLiteLegacy
{
public static readonly SQLiteLegacy Instance;
private readonly ReaderWriterLockSlim m_ConnectionLock;
private readonly SQLiteConnection m_Connection;
static SQLiteLegacy()
{
Instance = new SQLiteLegacy();
}
public SQLiteLegacy()
{
m_ConnectionLock = new ReaderWriterLockSlim();
var dataSource = Program.ConfigLocation;
m_Connection = new SQLiteConnection($"Data Source=\"{dataSource}\";Version=3;PRAGMA locking_mode=NORMAL;PRAGMA busy_timeout=5000", true);
}
internal void Init()
{
m_Connection.Open();
}
internal void Exit()
{
m_Connection.Close();
m_Connection.Dispose();
}
public void Execute(IJavascriptCallback callback, string sql, IDictionary<string, object> args = null)
{
try
{
m_ConnectionLock.EnterReadLock();
try
{
using (var command = new SQLiteCommand(sql, m_Connection))
{
if (args != null)
{
foreach (var arg in args)
{
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
}
}
using (var reader = command.ExecuteReader())
{
while (reader.Read() == true)
{
var values = new object[reader.FieldCount];
reader.GetValues(values);
if (callback.CanExecute == true)
{
callback.ExecuteAsync(null, values);
}
}
}
}
if (callback.CanExecute == true)
{
callback.ExecuteAsync(null, null);
}
}
finally
{
m_ConnectionLock.ExitReadLock();
}
}
catch (Exception e)
{
if (callback.CanExecute == true)
{
callback.ExecuteAsync(e.Message, null);
}
}
callback.Dispose();
}
public void Execute(Action<object[]> callback, string sql, IDictionary<string, object> args = null)
{
m_ConnectionLock.EnterReadLock();
try
{
using (var command = new SQLiteCommand(sql, m_Connection))
{
if (args != null)
{
foreach (var arg in args)
{
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
}
}
using (var reader = command.ExecuteReader())
{
while (reader.Read() == true)
{
var values = new object[reader.FieldCount];
reader.GetValues(values);
callback(values);
}
}
}
}
catch
{
}
finally
{
m_ConnectionLock.ExitReadLock();
}
}
public int ExecuteNonQuery(string sql, IDictionary<string, object> args = null)
{
int result = -1;
m_ConnectionLock.EnterWriteLock();
try
{
using (var command = new SQLiteCommand(sql, m_Connection))
{
if (args != null)
{
foreach (var arg in args)
{
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
}
}
result = command.ExecuteNonQuery();
}
}
finally
{
m_ConnectionLock.ExitWriteLock();
}
return result;
}
}
}
@@ -0,0 +1,723 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NLog;
namespace VRCX
{
internal static class ScreenshotHelper
{
private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
private static readonly byte[] pngSignatureBytes = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
private static readonly ScreenshotMetadataDatabase cacheDatabase = new ScreenshotMetadataDatabase(Path.Combine(Program.AppDataDirectory, "metadataCache.db"));
private static readonly Dictionary<string, ScreenshotMetadata> metadataCache = new Dictionary<string, ScreenshotMetadata>();
public enum ScreenshotSearchType
{
Username,
UserID,
WorldName,
WorldID,
}
public static bool TryGetCachedMetadata(string filePath, out ScreenshotMetadata metadata)
{
if (metadataCache.TryGetValue(filePath, out metadata))
return true;
int id = cacheDatabase.IsFileCached(filePath);
if (id != -1)
{
string metadataStr = cacheDatabase.GetMetadataById(id);
var metadataObj = metadataStr == null ? null : JsonConvert.DeserializeObject<ScreenshotMetadata>(metadataStr);
metadataCache.Add(filePath, metadataObj);
metadata = metadataObj;
return true;
}
return false;
}
public static List<ScreenshotMetadata> FindScreenshots(string query, string directory, ScreenshotSearchType searchType)
{
var result = new List<ScreenshotMetadata>();
var files = Directory.GetFiles(directory, "*.png", SearchOption.AllDirectories);
var addToCache = new List<MetadataCache>();
int amtFromCache = 0;
foreach (var file in files)
{
ScreenshotMetadata metadata = null;
if (TryGetCachedMetadata(file, out metadata))
{
amtFromCache++;
}
else
{
metadata = GetScreenshotMetadata(file, false);
var dbEntry = new MetadataCache()
{
FilePath = file,
Metadata = null,
CachedAt = DateTimeOffset.Now
};
if (metadata == null || metadata.Error != null)
{
addToCache.Add(dbEntry);
metadataCache.Add(file, null);
continue;
}
dbEntry.Metadata = JsonConvert.SerializeObject(metadata);
addToCache.Add(dbEntry);
metadataCache.Add(file, metadata);
}
if (metadata == null) continue;
switch (searchType)
{
case ScreenshotSearchType.Username:
if (metadata.ContainsPlayerName(query, true, true))
result.Add(metadata);
break;
case ScreenshotSearchType.UserID:
if (metadata.ContainsPlayerID(query))
result.Add(metadata);
break;
case ScreenshotSearchType.WorldName:
if (metadata.World.Name.IndexOf(query, StringComparison.OrdinalIgnoreCase) != -1)
result.Add(metadata);
break;
case ScreenshotSearchType.WorldID:
if (metadata.World.Id == query)
result.Add(metadata);
break;
}
}
if (addToCache.Count > 0)
cacheDatabase.BulkAddMetadataCache(addToCache);
logger.ConditionalDebug("Found {0}/{1} screenshots matching query '{2}' of type '{3}'. {4}/{5} pulled from cache.", result.Count, files.Length, query, searchType, amtFromCache, files.Length);
return result;
}
/// <summary>
/// Retrieves metadata from a PNG screenshot file and attempts to parse it.
/// </summary>
/// <param name="path">The path to the PNG screenshot file.</param>
/// <returns>A JObject containing the metadata or null if no metadata was found.</returns>
public static ScreenshotMetadata GetScreenshotMetadata(string path, bool includeJSON = false)
{
// Early return if file doesn't exist, or isn't a PNG(Check both extension and file header)
if (!File.Exists(path) || !path.EndsWith(".png") || !IsPNGFile(path))
return null;
///if (metadataCache.TryGetValue(path, out var cachedMetadata))
// return cachedMetadata;
string metadataString;
// Get the metadata string from the PNG file
try
{
metadataString = ReadPNGDescription(path);
}
catch (Exception ex)
{
logger.Error(ex, "Failed to read PNG description for file '{0}'", path);
return ScreenshotMetadata.JustError(path, "Failed to read PNG description. Check logs.");
}
// If the metadata string is empty for some reason, there's nothing to parse.
if (string.IsNullOrEmpty(metadataString))
return null;
// Check for specific metadata string start sequences
if (metadataString.StartsWith("lfs") || metadataString.StartsWith("screenshotmanager"))
{
try
{
var result = ScreenshotHelper.ParseLfsPicture(metadataString);
result.SourceFile = path;
return result;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to parse LFS/ScreenshotManager metadata for file '{0}'", path);
return ScreenshotMetadata.JustError(path, "Failed to parse LFS/ScreenshotManager metadata.");
}
}
// If not JSON metadata, return early so we're not throwing/catching pointless exceptions
if (!metadataString.StartsWith("{"))
{
logger.ConditionalDebug("Screenshot file '{0}' has unknown non-JSON metadata:\n{1}\n", path, metadataString);
return ScreenshotMetadata.JustError(path, "File has unknown non-JSON metadata.");
}
// Parse the metadata as VRCX JSON metadata
try
{
var result = JsonConvert.DeserializeObject<ScreenshotMetadata>(metadataString);
result.SourceFile = path;
if (includeJSON)
result.JSON = metadataString;
return result;
}
catch (JsonException ex)
{
logger.Error(ex, "Failed to parse screenshot metadata JSON for file '{0}'", path);
return ScreenshotMetadata.JustError(path, "Failed to parse screenshot metadata JSON. Check logs.");
}
}
/// <summary>
/// Writes a text description into a PNG file at the specified path.
/// Creates an iTXt PNG chunk in the target file, using the Description tag, with the specified text.
/// </summary>
/// <param name="path">The file path of the PNG file in which the description is to be written.</param>
/// <param name="text">The text description that is to be written into the PNG file.</param>
/// <returns>
/// <c>true</c> if the text description is successfully written to the PNG file;
/// otherwise, <c>false</c>.
/// </returns>
public static bool WritePNGDescription(string path, string text)
{
if (!File.Exists(path) || !IsPNGFile(path)) return false;
var png = File.ReadAllBytes(path);
var newChunkIndex = FindEndOfChunk(png, "IHDR");
if (newChunkIndex == -1) return false;
// If this file already has a text chunk, chances are it got logged twice for some reason. Stop.
var existingiTXt = FindChunkIndex(png, "iTXt");
if (existingiTXt != -1) return false;
var newChunk = new PNGChunk("iTXt");
newChunk.InitializeTextChunk("Description", text);
var newFile = png.ToList();
newFile.InsertRange(newChunkIndex, newChunk.ConstructChunkByteArray());
File.WriteAllBytes(path, newFile.ToArray());
return true;
}
/// <summary>
/// Reads a text description from a PNG file at the specified path.
/// Reads any existing iTXt PNG chunk in the target file, using the Description tag.
/// </summary>
/// <param name="path">The file path of the PNG file in which the description is to be read from.</param>
/// <returns>
/// The text description that is read from the PNG file.
/// </returns>
public static string ReadPNGDescription(string path)
{
if (!File.Exists(path) || !IsPNGFile(path)) return null;
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512))
{
var existingiTXt = FindChunk(stream, "iTXt", true);
if (existingiTXt == null) return null;
return existingiTXt.GetText("Description");
}
}
/// <summary>
/// Reads the PNG resolution.
/// </summary>
/// <param name="path">The path.</param>
/// <returns></returns>
public static string ReadPNGResolution(string path)
{
if (!File.Exists(path) || !IsPNGFile(path)) return null;
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 512))
{
var existingiHDR = FindChunk(stream, "IHDR", false);
if (existingiHDR == null) return null;
return existingiHDR.GetResolution();
}
}
/// <summary>
/// Determines whether the specified file is a PNG file. We do this by checking if the first 8 bytes in the file path match the PNG signature.
/// </summary>
/// <param name="path">The path of the file to check.</param>
/// <returns></returns>
public static bool IsPNGFile(string path)
{
// Read only the first 8 bytes of the file to check if it's a PNG file instead of reading the entire thing into memory just to see check a couple bytes.
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
if (fs.Length < 33) return false; // I don't remember how I came up with this number, but a PNG file below this size is not going to be valid for our purposes.
var signature = new byte[8];
fs.Read(signature, 0, 8);
return signature.SequenceEqual(pngSignatureBytes);
}
}
/// <summary>
/// Finds the index of the first of a specified chunk type in the specified PNG file.
/// </summary>
/// <param name="png">Array of bytes representing a PNG file.</param>
/// <param name="type">Type of PMG chunk to find</param>
/// <returns></returns>
private static int FindChunkIndex(byte[] png, string type)
{
int chunksProcessed = 0;
int chunkSeekLimit = 5;
bool isLittleEndian = BitConverter.IsLittleEndian;
// The first 8 bytes of the file are the png signature, so we can skip them.
var index = 8;
while (index < png.Length)
{
var chunkLength = new byte[4];
Array.Copy(png, index, chunkLength, 0, 4);
// BitConverter wants little endian(unless your system is big endian for some reason), PNG multi-byte integers are big endian. So we reverse the array.
if (isLittleEndian) Array.Reverse(chunkLength);
var length = BitConverter.ToInt32(chunkLength, 0);
// We don't need to reverse strings since UTF-8 strings aren't affected by endianess, given that they're a sequence of bytes.
var chunkName = new byte[4];
Array.Copy(png, index + 4, chunkName, 0, 4);
var name = Encoding.UTF8.GetString(chunkName);
if (name == type)
{
return index;
}
if (name == "IEND") // Nothing should exist past IEND in a normal png file, so we should stop parsing here to avoid trying to parse junk data.
{
return -1;
}
// The chunk length is 4 bytes, the chunk name is 4 bytes, the chunk data is length bytes, and the chunk CRC is 4 bytes.
// We add 12 to the index to get to the start of the next chunk in the file on the next loop.
index += length + 12;
chunksProcessed++;
if (chunksProcessed > chunkSeekLimit) break;
}
return -1;
}
private static int FindChunkIndex(FileStream fs, string type, bool seekEnd)
{
int chunksProcessed = 0;
int chunkSeekLimit = 5;
bool isLittleEndian = BitConverter.IsLittleEndian;
fs.Seek(8, SeekOrigin.Begin);
byte[] buffer = new byte[8];
while (fs.Position < fs.Length)
{
int chunkIndex = (int)fs.Position;
fs.Read(buffer, 0, 8); // Read both chunkLength and chunkName at once into this buffer
// BitConverter wants little endian(unless your system is big endian for some reason), PNG multi-byte integers are big endian. So we reverse the array.
if (isLittleEndian) Array.Reverse(buffer, 0, 4); // Only reverse the chunkLength part
int chunkLength = BitConverter.ToInt32(buffer, 0);
string chunkType = Encoding.UTF8.GetString(buffer, 4, 4); // We don't need to reverse strings since UTF-8 strings aren't affected by endianess, given that they're a sequence of bytes.
if (chunkType == type) return chunkIndex;
if (chunkType == "IEND") return -1; // Nothing should exist past IEND in a normal png file, so we should stop parsing here to avoid trying to parse junk data.
// The chunk length is 4 bytes, the chunk name is 4 bytes, the chunk data is chunkLength bytes, and the chunk CRC after chunk data is 4 bytes.
// We've already read the length/type which is the first 8 bytes, so we'll seek the chunk length + 4(CRC) to get to the start of the next chunk in the file.
fs.Seek(chunkLength + 4, SeekOrigin.Current);
chunksProcessed++;
if (chunksProcessed > chunkSeekLimit) break;
}
// If we've processed more than 5 chunks and still haven't found the chunk we're looking for, we'll start searching from the end of the file.
// We start at an offset of 12 since the IEND chunk (should) always be the last chunk in the file, be 12 bytes, and we don't need to check it.
fs.Seek(-12, SeekOrigin.End);
// We're going to read the last 4096 bytes of the file, which (should) be enough to find any trailing iTXt chunks we're looking for.
// If an LFS screenshots has the metadata of like 80 players attached to it, this likely won't be enough to find the iTXt chunk.
// I don't have any screenshots with that much metadata to test with and will not create them manually, so I'm not going to worry about it for now.
var chunkNameBytes = Encoding.UTF8.GetBytes(type);
fs.Seek(-4096, SeekOrigin.Current);
byte[] trailingBytes = new byte[4096];
fs.Read(trailingBytes, 0, 4096);
// At this scale we can just brute force/naive search for the chunk name in the trailing bytes and performance will be fine.
for (int i = 0; i <= trailingBytes.Length - chunkNameBytes.Length; i++)
{
bool isMatch = true;
for (int j = 0; j < chunkNameBytes.Length; j++)
{
if (trailingBytes[i + j] != chunkNameBytes[j])
{
isMatch = false;
break;
}
}
if (isMatch)
{
return (int)fs.Position - 4096 + i - 4;
}
}
return -1;
}
/// <summary>
/// Finds the index of the end of the specified chunk type in the specified PNG file.
/// </summary>
/// <param name="png">Array of bytes representing a PNG file.</param>
/// <param name="type">Type of PMG chunk to find</param>
/// <returns></returns>
private static int FindEndOfChunk(byte[] png, string type)
{
var index = FindChunkIndex(png, type);
if (index == -1) return index;
var chunkLength = new byte[4];
Array.Copy(png, index, chunkLength, 0, 4);
Array.Reverse(chunkLength);
var length = BitConverter.ToInt32(chunkLength, 0);
return index + length + 12;
}
/// <summary>
/// Finds the specified chunk type in the specified PNG file and returns it as a PNGChunk.
/// </summary>
/// <param name="png">Array of bytes representing a PNG file</param>
/// <param name="type">Type of PMG chunk to find</param>
/// <returns>PNGChunk</returns>
private static PNGChunk FindChunk(byte[] png, string type)
{
var index = FindChunkIndex(png, type);
if (index == -1) return null;
var chunkLength = new byte[4];
Array.Copy(png, index, chunkLength, 0, 4);
Array.Reverse(chunkLength);
var length = BitConverter.ToInt32(chunkLength, 0);
var chunkData = new byte[length];
Array.Copy(png, index + 8, chunkData, 0, length);
return new PNGChunk(type, chunkData);
}
/// <summary>
/// Finds the specified chunk type in the specified PNG file and returns it as a PNGChunk.
/// </summary>
/// <param name="fs">FileStream of a PNG file.</param>
/// <param name="type">Type of PMG chunk to find</param>
/// <returns>PNGChunk</returns>
private static PNGChunk FindChunk(FileStream fs, string type, bool seekFromEnd)
{
var index = FindChunkIndex(fs, type, seekFromEnd);
if (index == -1) return null;
// Seek back to start of found chunk
fs.Seek(index, SeekOrigin.Begin);
var chunkLength = new byte[4];
fs.Read(chunkLength, 0, 4);
Array.Reverse(chunkLength);
var length = BitConverter.ToInt32(chunkLength, 0);
// Skip the chunk type bytes
fs.Seek(4, SeekOrigin.Current);
var chunkData = new byte[length];
fs.Read(chunkData, 0, length);
return new PNGChunk(type, chunkData);
}
/// <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 ScreenshotMetadata ParseLfsPicture(string metadataString)
{
var metadata = new ScreenshotMetadata();
// LFS v2 format: https://github.com/knah/VRCMods/blob/c7e84936b52b6f476db452a37ab889eabe576845/LagFreeScreenshots/API/MetadataV2.cs#L35
// Normal entry
// lfs|2|author:usr_032383a7-748c-4fb2-94e4-bcb928e5de6b,Natsumi-sama|world:wrld_b016712b-5ce6-4bcb-9144-c8ed089b520f,35372,pet park test|pos:-60.49379,-0.002925932,5.805772|players:usr_9d73bff9-4543-4b6f-a004-9e257869ff50,-0.85,-0.17,-0.58,Olivia.;usr_3097f91e-a816-4c7a-a625-38fbfdee9f96,12.30,13.72,0.08,Zettai Ryouiki;usr_032383a7-748c-4fb2-94e4-bcb928e5de6b,0.68,0.32,-0.28,Natsumi-sama;usr_7525f45f-517e-442b-9abc-fbcfedb29f84,0.51,0.64,0.70,Weyoun
// Entry with image rotation enabled (rq:)
// lfs|2|author:usr_8c0a2f22-26d4-4dc9-8396-2ab40e3d07fc,knah|world:wrld_fb4edc80-6c48-43f2-9bd1-2fa9f1345621,35341,Luminescent Ledge|pos:8.231676,0.257298,-0.1983307|rq:2|players:usr_65b9eeeb-7c91-4ad2-8ce4-addb1c161cd6,0.74,0.59,1.57,Jakkuba;usr_6a50647f-d971-4281-90c3-3fe8caf2ba80,8.07,9.76,0.16,SopwithPup;usr_8c0a2f22-26d4-4dc9-8396-2ab40e3d07fc,0.26,1.03,-0.28,knah;usr_7f593ad1-3e9e-4449-a623-5c1c0a8d8a78,0.15,0.60,1.46,NekOwneD
// LFS v1 format: https://github.com/knah/VRCMods/blob/23c3311fdfc4af4b568eedfb2e366710f2a9f925/LagFreeScreenshots/LagFreeScreenshotsMod.cs
// Why support this tho
// lfs|1|world:wrld_6caf5200-70e1-46c2-b043-e3c4abe69e0f:47213,The Great Pug|players:usr_290c03d6-66cc-4f0e-b782-c07f5cfa8deb,VirtualTeacup;usr_290c03d6-66cc-4f0e-b782-c07f5cfa8deb,VirtualTeacup
// LFS CVR Edition v1 format: https://github.com/dakyneko/DakyModsCVR/blob/48eecd1bccd1a5b2ea844d899d59cf1186ec9912/LagFreeScreenshots/API/MetadataV2.cs#L41
// lfs|cvr|1|author:047b30bd-089d-887c-8734-b0032df5d176,Hordini|world:2e73b387-c6d4-45e9-b998-0fd6aa122c1d,i+efec20004ef1cd8b-404003-93833f-1aee112a,Bono's Basement (Anime) (#816724)|pos:2.196716,0.01250899,-3.817466|players:5301af21-eb8d-7b36-3ef4-b623fa51c2c6,3.778407,0.01250887,-3.815876,DDAkebono;f9e5c36c-41b0-7031-1185-35b4034010c0,4.828233,0.01250893,-3.920135,Natsumi
var lfsParts = metadataString.Split('|');
if (lfsParts[1] == "cvr")
lfsParts = lfsParts.Skip(1).ToArray();
var version = int.Parse(lfsParts[1]);
var application = lfsParts[0];
metadata.Application = application;
metadata.Version = version;
bool isCVR = application == "cvr";
if (application == "screenshotmanager")
{
// ScreenshotManager format: https://github.com/DragonPlayerX/ScreenshotManager/blob/33950b98003e795d29c68ce5fe1d86e7e65c92ad/ScreenshotManager/Core/FileDataHandler.cs#L94
// screenshotmanager|0|author:usr_290c03d6-66cc-4f0e-b782-c07f5cfa8deb,VirtualTeacup|wrld_6caf5200-70e1-46c2-b043-e3c4abe69e0f,47213,The Great Pug
var author = lfsParts[2].Split(',');
metadata.Author.Id = author[0];
metadata.Author.DisplayName = author[1];
var world = lfsParts[3].Split(',');
metadata.World.Id = world[0];
metadata.World.Name = world[2];
metadata.World.InstanceId = string.Join(":", world[0], world[1]); // worldId:instanceId format, same as vrcx format, just minimal
return metadata;
}
for (var i = 2; i < lfsParts.Length; i++)
{
var split = lfsParts[i].Split(':');
var key = split[0];
var value = split[1];
if (String.IsNullOrEmpty(value)) // One of my LFS files had an empty value for 'players:'. not pog
continue;
var parts = value.Split(',');
switch (key)
{
case "author":
metadata.Author.Id = isCVR ? string.Empty : parts[0];
metadata.Author.DisplayName = isCVR ? $"{parts[1]} ({parts[0]})" : parts[1];
break;
case "world":
metadata.World.Id = isCVR || version == 1 ? string.Empty : parts[0];
metadata.World.InstanceId = isCVR || version == 1 ? string.Empty : string.Join(":", parts[0], parts[1]); // worldId:instanceId format, same as vrcx format, just minimal
metadata.World.Name = isCVR ? $"{parts[2]} ({parts[0]})" : (version == 1 ? value : parts[2]);
break;
case "pos":
float.TryParse(parts[0], out float x);
float.TryParse(parts[1], out float y);
float.TryParse(parts[2], out float z);
metadata.Pos = new Vector3(x, y, z);
break;
// We don't use this, so don't parse it.
/*case "rq":
// Image rotation
metadata.Add("rq", value);
break;*/
case "players":
var playersArray = metadata.Players;
var players = value.Split(';');
foreach (var player in players)
{
var playerParts = player.Split(',');
float.TryParse(playerParts[1], out float x2);
float.TryParse(playerParts[2], out float y2);
float.TryParse(playerParts[3], out float z2);
var playerDetail = new ScreenshotMetadata.PlayerDetail
{
Id = isCVR ? string.Empty : playerParts[0],
DisplayName = isCVR ? $"{playerParts[4]} ({playerParts[0]})" : playerParts[4],
Pos = new Vector3(x2, y2, z2)
};
playersArray.Add(playerDetail);
}
break;
}
}
return metadata;
}
}
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html 4.2.3
// Basic PNG Chunk Structure: Length(int, 4 bytes) | Type (string, 4 bytes) | chunk data (Depends on type) | 32-bit CRC code (4 bytes)
// basic tEXt data structure: Keyword (1-79 bytes string) | Null separator (1 byte) | Text (x bytes)
// basic iTXt data structure: Keyword (1-79 bytes string) | Null separator (1 byte) | Compression flag (1 byte) | Compression method (1 byte) | Language tag (0-x bytes) | Null separator | Translated keyword (0-x bytes) | Null separator | Text (x bytes)
// Proper practice here for arbitrary image processing would be to check the PNG file being passed for any existing iTXt chunks with the same keyword that we're trying to use; If we find one, we replace that chunk's data instead of creating a new chunk.
// Luckily, VRChat should never do this! Bugs notwithstanding, we should never re-process a png file either. So we're just going to skip that logic.
// This code would be HORRIBLE for general parsing of PNG files/metadata. It's not really meant to do that, it's just meant to do exactly what we need it to do.
internal class PNGChunk
{
// crc lookup table
private static uint[] crcTable;
// init lookup table and store crc for iTXt
private static readonly uint iTXtCrc = Crc32(new[] { (byte)'i', (byte)'T', (byte)'X', (byte)'t' }, 0, 4, 0);
private readonly Encoding keywordEncoding = Encoding.GetEncoding("ISO-8859-1"); // ISO-8859-1/Latin1 is the encoding used for the keyword in text chunks.
public List<byte> ChunkDataBytes;
public int ChunkDataLength;
public string ChunkType;
public PNGChunk(string chunkType)
{
ChunkType = chunkType;
ChunkDataBytes = new List<byte>();
}
public PNGChunk(string chunkType, byte[] bytes)
{
ChunkType = chunkType;
ChunkDataBytes = bytes.ToList();
ChunkDataLength = bytes.Length;
}
/// <summary>
/// Initializes this PNGChunk's data in the format of an iTXt chunk with the specified keyword and text.
/// </summary>
/// <param name="keyword">Keyword for text chunk</param>
/// <param name="text">Text data for text chunk</param>
public void InitializeTextChunk(string keyword, string text)
{
// Create our chunk data byte array
ChunkDataBytes.AddRange(keywordEncoding.GetBytes(keyword)); // keyword
ChunkDataBytes.Add(0x0); // Null separator
ChunkDataBytes.Add(0x0); // Compression flag
ChunkDataBytes.Add(0x0); // Compression method
ChunkDataBytes.Add(0x0); // Null separator (skipping over language tag byte)
ChunkDataBytes.Add(0x0); // Null separator (skipping over translated keyword byte)
ChunkDataBytes.AddRange(Encoding.UTF8.GetBytes(text)); // our text
ChunkDataLength = ChunkDataBytes.Count;
}
/// <summary>
/// Constructs and returns a full, coherent PNG chunk from this PNGChunk's data.
/// </summary>
/// <returns>PNG chunk byte array</returns>
public byte[] ConstructChunkByteArray()
{
var chunk = new List<byte>();
var chunkLengthBytes = BitConverter.GetBytes(ChunkDataLength);
var chunkCRCBytes = BitConverter.GetBytes(Crc32(ChunkDataBytes.ToArray(), 0, ChunkDataLength, iTXtCrc));
// Reverse the chunk length bytes/CRC bytes if system is little endian since PNG integers are big endian
if (BitConverter.IsLittleEndian)
{
Array.Reverse(chunkLengthBytes);
Array.Reverse(chunkCRCBytes);
}
chunk.AddRange(chunkLengthBytes); // add data length
chunk.AddRange(Encoding.UTF8.GetBytes(ChunkType)); // add chunk type
chunk.AddRange(ChunkDataBytes); // Add chunk data
chunk.AddRange(chunkCRCBytes); // Add chunk CRC32 hash.
return chunk.ToArray();
}
/// <summary>
/// Gets the text from an iTXt chunk
/// </summary>
/// <param name="keyword">Keyword of the text chunk</param>
/// <returns>Text from chunk.</returns>
public string GetText(string keyword)
{
var offset = keywordEncoding.GetByteCount(keyword) + 5;
return Encoding.UTF8.GetString(ChunkDataBytes.ToArray(), offset, ChunkDataBytes.Count - offset);
}
public string GetResolution()
{
var x = BitConverter.ToInt32(ChunkDataBytes.Take(4).Reverse().ToArray(), 0);
var y = BitConverter.ToInt32(ChunkDataBytes.Skip(4).Take(4).Reverse().ToArray(), 0);
return $"{x}x{y}";
}
// Crc32 implementation from
// https://web.archive.org/web/20150825201508/http://upokecenter.dreamhosters.com/articles/png-image-encoder-in-c/
private static uint Crc32(byte[] stream, int offset, int length, uint crc)
{
uint c;
if (crcTable == null)
{
crcTable = new uint[256];
for (uint n = 0; n <= 255; n++)
{
c = n;
for (var k = 0; k <= 7; k++)
{
if ((c & 1) == 1)
c = 0xEDB88320 ^ ((c >> 1) & 0x7FFFFFFF);
else
c = (c >> 1) & 0x7FFFFFFF;
}
crcTable[n] = c;
}
}
c = crc ^ 0xffffffff;
var endOffset = offset + length;
for (var i = offset; i < endOffset; i++)
{
c = crcTable[(c ^ stream[i]) & 255] ^ ((c >> 8) & 0xFFFFFF);
}
return c ^ 0xffffffff;
}
}
}
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Newtonsoft.Json;
namespace VRCX
{
public class ScreenshotMetadata
{
/// <summary>
/// Name of the application writing to the screenshot. Should be VRCX.
/// </summary>
public string Application { get; set; }
/// <summary>
/// The version of this schema. If the format changes, this number should change.
/// </summary>
public int Version { get; set; }
/// <summary>
/// The details of the user that took the picture.
/// </summary>
public AuthorDetail Author { get; set; }
/// <summary>
/// Information about the world the picture was taken in.
/// </summary>
public WorldDetail World { get; set; }
/// <summary>
/// A list of players in the world at the time the picture was taken.
/// </summary>
public List<PlayerDetail> Players { get; set; }
/// <summary>
/// If this class was serialized from a file, this should be the path to the file.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
internal string SourceFile;
/// <summary>
/// The position of the player that took the picture when the shot was taken. Not written by VRCX, this is legacy support for reading LFS files.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Vector3? Pos { get; set; }
/// <summary>
/// Any error that occurred while parsing the file. This being true implies nothing else is set.
/// </summary>
[JsonIgnore]
internal string Error;
[JsonIgnore]
internal string JSON;
public ScreenshotMetadata()
{
Application = "VRCX";
Version = 1;
Author = new AuthorDetail();
World = new WorldDetail();
Players = new List<PlayerDetail>();
}
public static ScreenshotMetadata JustError(string sourceFile, string error)
{
return new ScreenshotMetadata
{
Error = error,
SourceFile = sourceFile
};
}
public bool ContainsPlayerID(string id)
{
return Players.Any(p => p.Id == id);
}
public bool ContainsPlayerName(string playerName, bool partial, bool ignoreCase)
{
var comparisonType = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (partial)
{
return Players.Any(p => p.DisplayName.IndexOf(playerName, comparisonType) != -1);
}
return Players.Any(p => p.DisplayName.Equals(playerName, comparisonType));
}
public class AuthorDetail
{
/// <summary>
/// The ID of the user.
/// </summary>
public string Id { get; set; }
/// <summary>
/// The display name of the user.
/// </summary>
public string DisplayName { get; set; }
}
public class WorldDetail
{
/// <summary>
/// The ID of the world.
/// </summary>
public string Id { get; set; }
/// <summary>
/// The name of the world.
/// </summary>
public string Name { get; set; }
/// <summary>
/// The full ID of the game instance.
/// </summary>
public string InstanceId { get; set; }
}
public class PlayerDetail
{
/// <summary>
/// The ID of the player in the world.
/// </summary>
public string Id { get; set; }
/// <summary>
/// The display name of the player in the world.
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// The position of the player in the world. Not written by VRCX, this is legacy support for reading LFS files.
/// </summary>
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public Vector3? Pos { get; set; } = null;
}
}
}
@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SQLite;
namespace VRCX
{
[Table("cache")]
public class MetadataCache
{
[PrimaryKey, AutoIncrement]
[Column("id")]
public int Id { get; set; }
[Column("file_path"), NotNull, Indexed]
public string FilePath { get; set; }
[Column("metadata")]
public string Metadata { get; set; }
[Column("cached_at"), NotNull]
public DateTimeOffset CachedAt { get; set; }
}
// Imagine using SQLite to store json strings in one table lmao
// Couldn't be me... oh wait
internal class ScreenshotMetadataDatabase
{
private SQLiteConnection sqlite;
public ScreenshotMetadataDatabase(string databaseLocation)
{
var options = new SQLiteConnectionString(databaseLocation, true);
sqlite = new SQLiteConnection(options);
sqlite.CreateTable<MetadataCache>();
}
public void AddMetadataCache(string filePath, string metadata)
{
var cache = new MetadataCache()
{
FilePath = filePath,
Metadata = metadata,
CachedAt = DateTimeOffset.Now
};
sqlite.Insert(cache);
}
public void BulkAddMetadataCache(IEnumerable<MetadataCache> cache)
{
sqlite.InsertAll(cache, runInTransaction: true);
}
public int IsFileCached(string filePath)
{
var query = sqlite.Table<MetadataCache>().Where(c => c.FilePath == filePath).Select(c => c.Id);
if (query.Any())
{
return query.First();
}
else
{
return -1;
}
}
public string GetMetadata(string filePath)
{
var query = sqlite.Table<MetadataCache>().Where(c => c.FilePath == filePath).Select(c => c.Metadata);
return query.FirstOrDefault();
}
public string GetMetadataById(int id)
{
var query = sqlite.Table<MetadataCache>().Where(c => c.Id == id).Select(c => c.Metadata);
return query.FirstOrDefault();
}
public void Close()
{
sqlite.Close();
}
}
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright(c) 2019-2022 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();
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
// Copyright(c) 2019-2022 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;
using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using System.Windows.Forms;
namespace VRCX
{
internal class StartupArgs
{
public static string LaunchCommand;
public static Process[] processList;
public static void ArgsCheck()
{
var args = Environment.GetCommandLineArgs();
processList = Process.GetProcessesByName("VRCX");
var isDebug = false;
Debug.Assert(isDebug = true);
foreach (var arg in args)
{
if (arg.Contains("--gpufix"))
Program.GPUFix = true;
if (arg.Length > 12 && arg.Substring(0, 12) == "/uri=vrcx://")
LaunchCommand = arg.Substring(12);
if (arg.Length > 8 && arg.Substring(0, 8) == "--config")
Program.ConfigLocation = arg.Substring(9);
if ((arg.Length >= 7 && arg.Substring(0, 7) == "--debug") || isDebug)
Program.LaunchDebug = true;
}
if (processList.Length > 1 && string.IsNullOrEmpty(LaunchCommand))
{
var result = MessageBox.Show("VRCX is already running, start another instance?", "VRCX", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
if (result == DialogResult.Yes)
return;
}
if (processList.Length > 1)
{
IPCToMain();
Environment.Exit(0);
}
}
private static void IPCToMain()
{
new IPCServer().CreateIPCServer();
var ipcClient = new NamedPipeClientStream(".", "vrcx-ipc", PipeDirection.InOut);
ipcClient.Connect();
if (ipcClient.IsConnected)
{
var buffer = Encoding.UTF8.GetBytes($"{{\"type\":\"LaunchCommand\",\"command\":\"{LaunchCommand}\"}}" + (char)0x00);
ipcClient.BeginWrite(buffer, 0, buffer.Length, IPCClient.Close, ipcClient);
}
}
}
}
+57
View File
@@ -0,0 +1,57 @@
// Copyright(c) 2019-2022 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;
using System.IO;
using System.Windows.Forms;
using System.Diagnostics;
namespace VRCX
{
internal class Update
{
private static readonly string VRCX_Setup_Executable = Path.Combine(Program.AppDataDirectory, "VRCX_Setup.exe");
private static readonly string Update_Executable = Path.Combine(Program.AppDataDirectory, "update.exe");
public static void Check()
{
if (Process.GetProcessesByName("VRCX_Setup").Length > 0)
Environment.Exit(0);
var setupHash = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
if (File.Exists(setupHash))
File.Delete(setupHash);
var tempDownload = Path.Combine(Program.AppDataDirectory, "tempDownload.exe");
if (File.Exists(tempDownload))
File.Delete(tempDownload);
if (File.Exists(VRCX_Setup_Executable))
File.Delete(VRCX_Setup_Executable);
if (File.Exists(Update_Executable))
Install();
}
public static void Install()
{
try
{
File.Move(Update_Executable, VRCX_Setup_Executable);
var VRCXProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = VRCX_Setup_Executable,
Arguments = "/S"
}
};
VRCXProcess.Start();
Environment.Exit(0);
}
catch (Exception e)
{
MessageBox.Show(e.ToString(), "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
using System;
using CefSharp;
namespace VRCX
{
public static class Util
{
public static void ApplyJavascriptBindings(IJavascriptObjectRepository repository)
{
repository.NameConverter = null;
repository.Register("AppApi", AppApi.Instance, true);
repository.Register("SharedVariable", SharedVariable.Instance, false);
repository.Register("WebApi", WebApi.Instance, true);
repository.Register("VRCXStorage", VRCXStorage.Instance, true);
repository.Register("SQLite", SQLiteLegacy.Instance, true);
repository.Register("LogWatcher", LogWatcher.Instance, true);
repository.Register("Discord", Discord.Instance, true);
repository.Register("AssetBundleCacher", AssetBundleCacher.Instance, true);
}
}
}
+140
View File
@@ -0,0 +1,140 @@
// Copyright(c) 2019-2022 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;
using System.Collections.Generic;
using System.IO;
using System.Threading;
namespace VRCX
{
public class VRCXStorage
{
public static readonly VRCXStorage Instance;
private static readonly ReaderWriterLockSlim m_Lock = new ReaderWriterLockSlim();
private static Dictionary<string, string> m_Storage = new Dictionary<string, string>();
private static readonly string m_JsonPath = Path.Combine(Program.AppDataDirectory, "VRCX.json");
private static bool m_Dirty;
static VRCXStorage()
{
Instance = new VRCXStorage();
}
public static void Load()
{
m_Lock.EnterWriteLock();
try
{
JsonSerializer.Deserialize(m_JsonPath, ref m_Storage);
m_Dirty = false;
}
finally
{
m_Lock.ExitWriteLock();
}
}
public static void Save()
{
m_Lock.EnterReadLock();
try
{
if (m_Dirty)
{
JsonSerializer.Serialize(m_JsonPath, m_Storage);
m_Dirty = false;
}
}
finally
{
m_Lock.ExitReadLock();
}
}
public void Flush()
{
Save();
}
public void Clear()
{
m_Lock.EnterWriteLock();
try
{
if (m_Storage.Count > 0)
{
m_Storage.Clear();
m_Dirty = true;
}
}
finally
{
m_Lock.ExitWriteLock();
}
}
public bool Remove(string key)
{
m_Lock.EnterWriteLock();
try
{
var result = m_Storage.Remove(key);
if (result)
{
m_Dirty = true;
}
return result;
}
finally
{
m_Lock.ExitWriteLock();
}
}
public string Get(string key)
{
m_Lock.EnterReadLock();
try
{
return m_Storage.TryGetValue(key, out string value)
? value
: string.Empty;
}
finally
{
m_Lock.ExitReadLock();
}
}
public void Set(string key, string value)
{
m_Lock.EnterWriteLock();
try
{
m_Storage[key] = value;
m_Dirty = true;
}
finally
{
m_Lock.ExitWriteLock();
}
}
public string GetAll()
{
m_Lock.EnterReadLock();
try
{
return System.Text.Json.JsonSerializer.Serialize(m_Storage);
}
finally
{
m_Lock.ExitReadLock();
}
}
}
}
+382
View File
@@ -0,0 +1,382 @@
using CefSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace VRCX
{
public class WebApi
{
public static readonly WebApi Instance;
public CookieContainer _cookieContainer;
private bool _cookieDirty;
private Timer _timer;
static WebApi()
{
Instance = new WebApi();
ServicePointManager.DefaultConnectionLimit = 10;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
}
public WebApi()
{
_cookieContainer = new CookieContainer();
_timer = new Timer(TimerCallback, null, -1, -1);
}
private void TimerCallback(object state)
{
try
{
SaveCookies();
}
catch
{
}
}
internal void Init()
{
LoadCookies();
_timer.Change(1000, 1000);
}
internal void Exit()
{
_timer.Change(-1, -1);
SaveCookies();
}
public void ClearCookies()
{
_cookieContainer = new CookieContainer();
SaveCookies();
}
internal void LoadCookies()
{
SQLiteLegacy.Instance.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS `cookies` (`key` TEXT PRIMARY KEY, `value` TEXT)");
SQLiteLegacy.Instance.Execute((values) =>
{
try
{
using (var stream = new MemoryStream(Convert.FromBase64String((string)values[0])))
{
_cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(stream);
}
}
catch
{
}
},
"SELECT `value` FROM `cookies` WHERE `key` = @key",
new Dictionary<string, object>() {
{"@key", "default"}
}
);
}
internal void SaveCookies()
{
if (_cookieDirty == false)
{
return;
}
try
{
using (var memoryStream = new MemoryStream())
{
new BinaryFormatter().Serialize(memoryStream, _cookieContainer);
SQLiteLegacy.Instance.ExecuteNonQuery(
"INSERT OR REPLACE INTO `cookies` (`key`, `value`) VALUES (@key, @value)",
new Dictionary<string, object>() {
{"@key", "default"},
{"@value", Convert.ToBase64String(memoryStream.ToArray())}
}
);
}
_cookieDirty = false;
}
catch
{
}
}
public string GetCookies()
{
_cookieDirty = true; // force cookies to be saved for lastUserLoggedIn
using (var memoryStream = new MemoryStream())
{
new BinaryFormatter().Serialize(memoryStream, _cookieContainer);
return Convert.ToBase64String(memoryStream.ToArray());
}
}
public void SetCookies(string cookies)
{
using (var stream = new MemoryStream(Convert.FromBase64String(cookies)))
{
_cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(stream);
}
_cookieDirty = true; // force cookies to be saved for lastUserLoggedIn
}
private static async Task LegacyImageUpload(HttpWebRequest request, IDictionary<string, object> options)
{
request.Method = "POST";
string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
request.ContentType = "multipart/form-data; boundary=" + boundary;
Stream requestStream = request.GetRequestStream();
if (options.TryGetValue("postData", out object postDataObject) == true)
{
Dictionary<string, string> postData = new Dictionary<string, string>();
postData.Add("data", (string)postDataObject);
string FormDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n";
foreach (string key in postData.Keys)
{
string item = string.Format(FormDataTemplate, boundary, key, postData[key]);
byte[] itemBytes = Encoding.UTF8.GetBytes(item);
await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length);
}
}
var imageData = options["imageData"] as string;
byte[] fileToUpload = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length);
string fileFormKey = "image";
string fileName = "image.png";
string fileMimeType = "image/png";
string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n";
string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType);
byte[] headerbytes = Encoding.UTF8.GetBytes(header);
await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length);
using (MemoryStream fileStream = new MemoryStream(fileToUpload))
{
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
{
await requestStream.WriteAsync(buffer, 0, bytesRead);
}
fileStream.Close();
}
byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n");
await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length);
byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--");
await requestStream.WriteAsync(endBytes, 0, endBytes.Length);
requestStream.Close();
}
private static async Task UploadFilePut(HttpWebRequest request, IDictionary<string, object> options)
{
request.Method = "PUT";
request.ContentType = options["fileMIME"] as string;
var fileData = options["fileData"] as string;
var sentData = Convert.FromBase64CharArray(fileData.ToCharArray(), 0, fileData.Length);
request.ContentLength = sentData.Length;
using (var sendStream = request.GetRequestStream())
{
await sendStream.WriteAsync(sentData, 0, sentData.Length);
sendStream.Close();
}
}
private static async Task ImageUpload(HttpWebRequest request, IDictionary<string, object> options)
{
request.Method = "POST";
string boundary = "---------------------------" + DateTime.Now.Ticks.ToString("x");
request.ContentType = "multipart/form-data; boundary=" + boundary;
Stream requestStream = request.GetRequestStream();
if (options.TryGetValue("postData", out object postDataObject))
{
var jsonPostData = (JObject)JsonConvert.DeserializeObject((string)postDataObject);
Dictionary<string, string> postData = new Dictionary<string, string>();
string formDataTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}\r\n";
if (jsonPostData != null)
{
foreach (var data in jsonPostData)
{
string item = string.Format(formDataTemplate, boundary, data.Key, data.Value);
byte[] itemBytes = Encoding.UTF8.GetBytes(item);
await requestStream.WriteAsync(itemBytes, 0, itemBytes.Length);
}
}
}
var imageData = options["imageData"] as string;
byte[] fileToUpload = Convert.FromBase64CharArray(imageData.ToCharArray(), 0, imageData.Length);
string fileFormKey = "file";
string fileName = "blob";
string fileMimeType = "image/png";
string HeaderTemplate = "--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n";
string header = string.Format(HeaderTemplate, boundary, fileFormKey, fileName, fileMimeType);
byte[] headerbytes = Encoding.UTF8.GetBytes(header);
await requestStream.WriteAsync(headerbytes, 0, headerbytes.Length);
using (MemoryStream fileStream = new MemoryStream(fileToUpload))
{
byte[] buffer = new byte[1024];
int bytesRead = 0;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0)
{
await requestStream.WriteAsync(buffer, 0, bytesRead);
}
fileStream.Close();
}
byte[] newlineBytes = Encoding.UTF8.GetBytes("\r\n");
await requestStream.WriteAsync(newlineBytes, 0, newlineBytes.Length);
byte[] endBytes = Encoding.UTF8.GetBytes("--" + boundary + "--");
await requestStream.WriteAsync(endBytes, 0, endBytes.Length);
requestStream.Close();
}
#pragma warning disable CS4014
public async void Execute(IDictionary<string, object> options, IJavascriptCallback callback)
{
try
{
var request = WebRequest.CreateHttp((string)options["url"]);
request.CookieContainer = _cookieContainer;
request.KeepAlive = true;
request.UserAgent = Program.Version;
if (options.TryGetValue("headers", out object headers))
{
foreach (var header in (IEnumerable<KeyValuePair<string, object>>)headers)
{
var key = header.Key;
var value = header.Value.ToString();
if (string.Compare(key, "Content-Type", StringComparison.OrdinalIgnoreCase) == 0)
{
request.ContentType = value;
}
else if (string.Compare(key, "Referer", StringComparison.OrdinalIgnoreCase) == 0)
{
request.Referer = value;
}
else
{
request.Headers.Add(key, value);
}
}
}
if (options.TryGetValue("method", out object method))
{
var _method = (string)method;
request.Method = _method;
if (string.Compare(_method, "GET", StringComparison.OrdinalIgnoreCase) != 0 &&
options.TryGetValue("body", out object body) == true)
{
using (var stream = await request.GetRequestStreamAsync())
using (var streamWriter = new StreamWriter(stream))
{
await streamWriter.WriteAsync((string)body);
}
}
}
if (options.TryGetValue("uploadImage", out _))
{
await ImageUpload(request, options);
}
if (options.TryGetValue("uploadFilePUT", out _))
{
await UploadFilePut(request, options);
}
if (options.TryGetValue("uploadImageLegacy", out _))
{
await LegacyImageUpload(request, options);
}
try
{
using (var response = await request.GetResponseAsync() as HttpWebResponse)
{
if (response.Headers["Set-Cookie"] != null)
{
_cookieDirty = true;
}
using (var stream = response.GetResponseStream())
using (var streamReader = new StreamReader(stream))
{
if (callback.CanExecute == true)
{
if (response.ContentType.Contains("image/") || response.ContentType.Contains("application/octet-stream"))
{
// base64 response data for image
using (var memoryStream = new MemoryStream())
{
await stream.CopyToAsync(memoryStream);
callback.ExecuteAsync(null, new
{
data = $"data:image/png;base64,{Convert.ToBase64String(memoryStream.ToArray())}",
status = response.StatusCode
});
}
}
else
{
callback.ExecuteAsync(null, new
{
data = await streamReader.ReadToEndAsync(),
status = response.StatusCode
});
}
}
}
}
}
catch (WebException webException)
{
if (webException.Response is HttpWebResponse response)
{
if (response.Headers["Set-Cookie"] != null)
{
_cookieDirty = true;
}
using (var stream = response.GetResponseStream())
using (var streamReader = new StreamReader(stream))
{
if (callback.CanExecute == true)
{
callback.ExecuteAsync(null, new
{
data = await streamReader.ReadToEndAsync(),
status = response.StatusCode
});
}
}
}
else if (callback.CanExecute == true)
{
callback.ExecuteAsync(webException.Message, null);
}
}
}
catch (Exception e)
{
if (callback.CanExecute == true)
{
// FIXME: 브라우저는 종료되었는데 얘는 이후에 실행되면 터짐
callback.ExecuteAsync(e.Message, null);
}
}
callback.Dispose();
}
#pragma warning restore CS4014
}
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright(c) 2019-2022 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;
using System.Runtime.InteropServices;
namespace VRCX
{
public static class WinApi
{
[DllImport("kernel32.dll", SetLastError = false)]
public static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern int SHParseDisplayName([MarshalAs(UnmanagedType.LPWStr)] string pszName, IntPtr pbc, out IntPtr ppidl, uint sfgaoIn, out uint psfgaoOut);
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SHOpenFolderAndSelectItems(IntPtr pidlFolder, uint cidl, IntPtr[] apidl, uint dwFlags);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
/// <summary>
/// Flag that specifies the access rights to query limited information about a process.
/// This won't throw an exception when we try to access info about an elevated process
/// </summary>
private const int PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
/// <summary>
/// Determines whether the specified process has exited using WinAPI's GetExitCodeProcess running with PROCESS_QUERY_LIMITED_INFORMATION.
/// We do this because Process.HasExited in .net framework opens a handle with PROCESS_QUERY_INFORMATION, which will throw an exception if the process is elevated.
/// GetExitCodeProcess works with PROCESS_QUERY_LIMITED_INFORMATION, which will not throw an exception if the process is elevated.
/// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getexitcodeprocess
/// </summary>
/// <param name="process">The process to check.</param>
/// <returns>true if the process has exited; otherwise, false.</returns>
internal static bool HasProcessExited(int processId)
{
IntPtr hProcess = WinApi.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, processId);
if (hProcess == IntPtr.Zero)
{
// this is probably fine
return true;
//throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
bool exited;
try
{
if (!WinApi.GetExitCodeProcess(hProcess, out uint exitCode))
{
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
// Fun fact, If a program uses STILL_ACTIVE (259) as an exit code, GetExitCodeProcess will return 259, since it returns... the exit code. This would break this function.
exited = exitCode != 259;
}
finally
{
// Imagine closing process handles.
WinApi.CloseHandle(hProcess);
}
return exited;
}
}
}
+19
View File
@@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace VRCX
{
public class WinformBase : Form
{
protected override void OnHandleCreated(EventArgs e)
{
WinformThemer.SetThemeToGlobal(this);
base.OnHandleCreated(e);
}
}
}
+192
View File
@@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace VRCX
{
//Based off DWMWA_USE_IMMERSIVE_DARK_MODE, documentation: https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
//dwAttribute was 19 before Windows 20H1, 20 after Windows 20H1
internal static class WinformThemer
{
/// <summary>
/// Flash both the window caption and taskbar button.
/// This is equivalent to setting the FLASHW_CAPTION | FLASHW_TRAY flags.
/// </summary>
public const uint FLASHW_ALL = 3;
/// <summary>
/// Flash continuously until the window comes to the foreground.
/// </summary>
public const uint FLASHW_TIMERNOFG = 12;
/// <summary>
/// Private holder of current theme
/// </summary>
private static int currentTheme;
/// <summary>
/// Sets the global theme of the app
/// Light = 0
/// Dark = 1
/// </summary>
public static void SetGlobalTheme(int theme)
{
currentTheme = theme;
//Make a seperate list for all current forms (causes issues otherwise)
var forms = new List<Form>();
foreach (Form form in Application.OpenForms)
{
forms.Add(form);
}
SetThemeToGlobal(forms);
}
/// <summary>
/// Gets the global theme of the app
/// Light = 0
/// Dark = 1
/// </summary>
public static int GetGlobalTheme()
{
return currentTheme;
}
/// <summary>
/// Set given form to the current global theme
/// </summary>
/// <param name="form"></param>
public static void SetThemeToGlobal(Form form)
{
SetThemeToGlobal(new List<Form> { form });
}
/// <summary>
/// Set a list of given forms to the current global theme
/// </summary>
/// <param name="forms"></param>
public static void SetThemeToGlobal(List<Form> forms)
{
MainForm.Instance.Invoke(new Action(() =>
{
//For each form, set the theme, then move focus onto it to force refresh
foreach (var form in forms)
{
//Set the theme of the window
SetThemeToGlobal(form.Handle);
//Change opacity to foce full redraw
form.Opacity = 0.99999;
form.Opacity = 1;
}
}));
}
private static void SetThemeToGlobal(IntPtr handle)
{
if (GetTheme(handle) != currentTheme)
{
if (PInvoke.DwmSetWindowAttribute(handle, 19, new[] { currentTheme }, 4) != 0)
PInvoke.DwmSetWindowAttribute(handle, 20, new[] { currentTheme }, 4);
}
}
private static int GetTheme(IntPtr handle)
{
//Allocate needed memory
var curThemePtr = Marshal.AllocHGlobal(4);
//See what window state it currently is
if (PInvoke.DwmGetWindowAttribute(handle, 19, curThemePtr, 4) != 0)
PInvoke.DwmGetWindowAttribute(handle, 20, curThemePtr, 4);
//Read current theme (light = 0, dark = 1)
var theme = Marshal.ReadInt32(curThemePtr);
//Free previously allocated
Marshal.FreeHGlobal(curThemePtr);
return theme;
}
public static void DoFunny()
{
foreach (Form form in Application.OpenForms)
{
PInvoke.SetWindowLong(form.Handle, -20, 0x00C00000);
// PInvoke.SetWindowLong(form.Handle, -20, 0x00050100);
}
}
private static FLASHWINFO Create_FLASHWINFO(IntPtr handle, uint flags, uint count, uint timeout)
{
var fi = new FLASHWINFO();
fi.cbSize = Convert.ToUInt32(Marshal.SizeOf(fi));
fi.hwnd = handle;
fi.dwFlags = flags;
fi.uCount = count;
fi.dwTimeout = timeout;
return fi;
}
/// <summary>
/// Flash the spacified Window (Form) until it receives focus.
/// </summary>
/// <param name="form">The Form (Window) to Flash.</param>
/// <returns></returns>
public static bool Flash(Form form)
{
var fi = Create_FLASHWINFO(form.Handle, FLASHW_ALL | FLASHW_TIMERNOFG, uint.MaxValue, 0);
return PInvoke.FlashWindowEx(ref fi);
}
internal static class PInvoke
{
[DllImport("DwmApi")]
internal static extern int DwmSetWindowAttribute(IntPtr hwnd, int dwAttribute, int[] pvAttribute, int cbAttribute);
[DllImport("DwmApi")]
internal static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, IntPtr pvAttribute, int cbAttribute);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool FlashWindowEx(ref FLASHWINFO pwfi);
}
[StructLayout(LayoutKind.Sequential)]
internal struct FLASHWINFO
{
/// <summary>
/// The size of the structure in bytes.
/// </summary>
public uint cbSize;
/// <summary>
/// A Handle to the Window to be Flashed. The window can be either opened or minimized.
/// </summary>
public IntPtr hwnd;
/// <summary>
/// The Flash Status.
/// </summary>
public uint dwFlags;
/// <summary>
/// The number of times to Flash the window.
/// </summary>
public uint uCount;
/// <summary>
/// The rate at which the Window is to be flashed, in milliseconds. If Zero, the function uses the default cursor blink
/// rate.
/// </summary>
public uint dwTimeout;
}
}
}