mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 14:53:50 +02:00
Linux: SteamVR overlay support (#1299)
* fix: open folder and select item on linux * feat: linux wrist overlay * feat: linux hmd overlay * feat: replace unix sockets with shm on linux * fix: reduce linux wrist overlay fps * fix: hide electron offscreen windows * fix: destroy electron offscreen windows when not in use * fix: open folder and select item on linux * feat: cpu, uptime and device monitoring on linux * feat: native wayland gl context with x11 fallback on linux * fix: use platform agnostic wording for common folders * fix: crash dumps folder button on linux * fix: enable missing VR notification options on linux * fix: update cef, eslint config to include updated AppApiVr names * merge: rebase linux VR changes to upstream * Clean up * Load custom file contents rather than path Fixes loading custom file in debug mode * fix: call SetVR on linux as well * fix: AppApiVrElectron init, properly create and dispose of shm * Handle avatar history error * Lint * Change overlay dispose logic * macOS DOTNET_ROOT * Remove moving dotnet bin * Fix * fix: init overlay on SteamVR restart * Fix fetching empty instance, fix user dialog not fetching * Trim direct access inputs * Make icon higher res, because mac build would fail 😂 * macOS fixes * will it build? that's the question * fix: ensure offscreen windows are ready before vrinit * will it build? that's the question * will it build? that's the question * meow * one, more, time * Fix crash and overlay ellipsis * a --------- Co-authored-by: Natsumi <cmcooper123@hotmail.com>
This commit is contained in:
34
.github/workflows/github_actions.yml
vendored
34
.github/workflows/github_actions.yml
vendored
@@ -112,6 +112,38 @@ jobs:
|
|||||||
name: Electron-AppImage
|
name: Electron-AppImage
|
||||||
path: 'build/VRCX_${{ needs.set_version.outputs.version }}.AppImage'
|
path: 'build/VRCX_${{ needs.set_version.outputs.version }}.AppImage'
|
||||||
|
|
||||||
|
build_macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: [set_version, build_dotnet_linux, build_node]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set version
|
||||||
|
run: |
|
||||||
|
echo "${{ needs.set_version.outputs.version }}" > Version
|
||||||
|
cat Version
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: npm ci
|
||||||
|
- name: Build Electron-html
|
||||||
|
run: npm run prod-linux
|
||||||
|
- name: Download Electron dotnet artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Electron-Release
|
||||||
|
path: build/Electron
|
||||||
|
- name: Build macOS .dmg
|
||||||
|
run: npm run build-electron
|
||||||
|
- name: Upload Electron macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Electron-MacApp
|
||||||
|
path: 'build/VRCX_${{ needs.set_version.outputs.version }}.dmg'
|
||||||
|
|
||||||
create_setup:
|
create_setup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
@@ -122,7 +154,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set version
|
- name: Set version
|
||||||
run: |
|
run: |
|
||||||
echo "!define PRODUCT_VERSION_FROM_FILE \"${{ needs.set_version.outputs.date }}.0\"" > Installer/version_define.nsh
|
echo "!define PRODUCT_VERSION_FROM_FILE \"${{ needs.set_version.outputs.date }}.0\"" > Installer/version_define.nsh
|
||||||
- name: Install 7-zip and makensis
|
- name: Install 7-zip and makensis
|
||||||
run: sudo apt update && sudo apt install -y p7zip-full nsis nsis-pluginapi
|
run: sudo apt update && sudo apt install -y p7zip-full nsis nsis-pluginapi
|
||||||
- name: Set plugin permissions
|
- name: Set plugin permissions
|
||||||
|
|||||||
@@ -1,43 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using CefSharp;
|
using CefSharp;
|
||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
{
|
{
|
||||||
public class AppApiVr
|
public class AppApiVrCef : AppApiVr
|
||||||
{
|
{
|
||||||
public static readonly AppApiVr Instance;
|
static AppApiVrCef()
|
||||||
|
|
||||||
static AppApiVr()
|
|
||||||
{
|
{
|
||||||
Instance = new AppApiVr();
|
Instance = new AppApiVrCef();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Init()
|
public override void Init()
|
||||||
{
|
{
|
||||||
// Create Instance before Cef tries to bind it
|
// Create Instance before Cef tries to bind it
|
||||||
}
|
}
|
||||||
|
|
||||||
public void VrInit()
|
public override void VrInit()
|
||||||
{
|
{
|
||||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
|
MainForm.Instance.Browser.ExecuteScriptAsync("$app.store.vr.vrInit", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleSystemMonitor(bool enabled)
|
public override void ToggleSystemMonitor(bool enabled)
|
||||||
{
|
{
|
||||||
SystemMonitor.Instance.Start(enabled);
|
SystemMonitorCef.Instance.Start(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the current CPU usage as a percentage.
|
/// Returns the current CPU usage as a percentage.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The current CPU usage as a percentage.</returns>
|
/// <returns>The current CPU usage as a percentage.</returns>
|
||||||
public float CpuUsage()
|
public override float CpuUsage()
|
||||||
{
|
{
|
||||||
return SystemMonitor.Instance.CpuUsage;
|
return SystemMonitorCef.Instance.CpuUsage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -45,7 +43,7 @@ namespace VRCX
|
|||||||
/// Each sub-array contains the type of device and its current state
|
/// Each sub-array contains the type of device and its current state
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||||
public string[][] GetVRDevices()
|
public override string[][] GetVRDevices()
|
||||||
{
|
{
|
||||||
return Program.VRCXVRInstance.GetDevices();
|
return Program.VRCXVRInstance.GetDevices();
|
||||||
}
|
}
|
||||||
@@ -54,36 +52,42 @@ namespace VRCX
|
|||||||
/// Returns the number of milliseconds that the system has been running.
|
/// Returns the number of milliseconds that the system has been running.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The number of milliseconds that the system has been running.</returns>
|
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||||
public double GetUptime()
|
public override double GetUptime()
|
||||||
{
|
{
|
||||||
return SystemMonitor.Instance.UpTime;
|
return SystemMonitorCef.Instance.UpTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the current language of the operating system.
|
/// Returns the current language of the operating system.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The current language of the operating system.</returns>
|
/// <returns>The current language of the operating system.</returns>
|
||||||
public string CurrentCulture()
|
public override string CurrentCulture()
|
||||||
{
|
{
|
||||||
return CultureInfo.CurrentCulture.ToString();
|
return CultureInfo.CurrentCulture.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public override string CustomVrScript()
|
||||||
/// Returns the file path of the custom user js file, if it exists.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
|
||||||
public string CustomVrScriptPath()
|
|
||||||
{
|
{
|
||||||
var output = string.Empty;
|
|
||||||
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
output = filePath;
|
return File.ReadAllText(filePath);
|
||||||
return output;
|
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsRunningUnderWine()
|
public override bool IsRunningUnderWine()
|
||||||
{
|
{
|
||||||
return Wine.GetIfWine();
|
return Wine.GetIfWine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("GetExecuteVrFeedFunctionQueue is not implemented in AppApiVrCef.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("GetExecuteVrOverlayFunctionQueue is not implemented in AppApiVrCef.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,22 +96,22 @@ namespace VRCX
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CustomCssPath()
|
public string CustomCss()
|
||||||
{
|
{
|
||||||
var output = string.Empty;
|
|
||||||
var filePath = Path.Join(Program.AppDataDirectory, "custom.css");
|
var filePath = Path.Join(Program.AppDataDirectory, "custom.css");
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
output = filePath;
|
return File.ReadAllText(filePath);
|
||||||
return output;
|
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CustomScriptPath()
|
public string CustomScript()
|
||||||
{
|
{
|
||||||
var output = string.Empty;
|
|
||||||
var filePath = Path.Join(Program.AppDataDirectory, "custom.js");
|
var filePath = Path.Join(Program.AppDataDirectory, "custom.js");
|
||||||
if (File.Exists(filePath))
|
if (File.Exists(filePath))
|
||||||
output = filePath;
|
return File.ReadAllText(filePath);
|
||||||
return output;
|
|
||||||
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string CurrentCulture()
|
public string CurrentCulture()
|
||||||
|
|||||||
19
Dotnet/AppApi/Common/AppApiVrCommon.cs
Normal file
19
Dotnet/AppApi/Common/AppApiVrCommon.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace VRCX;
|
||||||
|
|
||||||
|
public abstract partial class AppApiVr
|
||||||
|
{
|
||||||
|
public static AppApiVr Instance;
|
||||||
|
public abstract void Init();
|
||||||
|
public abstract void VrInit();
|
||||||
|
public abstract void ToggleSystemMonitor(bool enabled);
|
||||||
|
public abstract float CpuUsage();
|
||||||
|
public abstract string[][] GetVRDevices();
|
||||||
|
public abstract double GetUptime();
|
||||||
|
public abstract string CurrentCulture();
|
||||||
|
public abstract string CustomVrScript();
|
||||||
|
public abstract bool IsRunningUnderWine();
|
||||||
|
public abstract List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
|
||||||
|
public abstract List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
|
||||||
|
}
|
||||||
@@ -9,27 +9,30 @@ namespace VRCX
|
|||||||
public partial class AppApiElectron : AppApi
|
public partial class AppApiElectron : AppApi
|
||||||
{
|
{
|
||||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
public override void ShowDevTools()
|
public override void ShowDevTools()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||||
{
|
{
|
||||||
|
Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RefreshVR()
|
public override void RefreshVR()
|
||||||
{
|
{
|
||||||
|
Program.VRCXVRInstance.Restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void RestartVR()
|
public override void RestartVR()
|
||||||
{
|
{
|
||||||
|
Program.VRCXVRInstance.Restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetZoom(double zoomLevel)
|
public override void SetZoom(double zoomLevel)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<double> GetZoom()
|
public override async Task<double> GetZoom()
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
@@ -47,23 +50,25 @@ namespace VRCX
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ExecuteAppFunction(string function, string json)
|
public override void ExecuteAppFunction(string function, string json)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ExecuteVrFeedFunction(string function, string json)
|
public override void ExecuteVrFeedFunction(string function, string json)
|
||||||
{
|
{
|
||||||
|
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||||
{
|
{
|
||||||
|
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void FocusWindow()
|
public override void FocusWindow()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void ChangeTheme(int value)
|
public override void ChangeTheme(int value)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -71,7 +76,7 @@ namespace VRCX
|
|||||||
public override void DoFunny()
|
public override void DoFunny()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string GetClipboard()
|
public override string GetClipboard()
|
||||||
{
|
{
|
||||||
var process = new Process
|
var process = new Process
|
||||||
@@ -102,7 +107,7 @@ namespace VRCX
|
|||||||
public override void SetStartup(bool enabled)
|
public override void SetStartup(bool enabled)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void CopyImageToClipboard(string path)
|
public override void CopyImageToClipboard(string path)
|
||||||
{
|
{
|
||||||
if (!File.Exists(path) ||
|
if (!File.Exists(path) ||
|
||||||
@@ -113,7 +118,7 @@ namespace VRCX
|
|||||||
!path.EndsWith(".bmp") &&
|
!path.EndsWith(".bmp") &&
|
||||||
!path.EndsWith(".webp")))
|
!path.EndsWith(".webp")))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var process = new Process
|
var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo
|
StartInfo = new ProcessStartInfo
|
||||||
@@ -124,7 +129,7 @@ namespace VRCX
|
|||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
process.Start();
|
process.Start();
|
||||||
process.WaitForExit();
|
process.WaitForExit();
|
||||||
@@ -138,16 +143,16 @@ namespace VRCX
|
|||||||
public override void FlashWindow()
|
public override void FlashWindow()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void SetUserAgent()
|
public override void SetUserAgent()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool IsRunningUnderWine()
|
public override bool IsRunningUnderWine()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
102
Dotnet/AppApi/Electron/AppApiVrElectron.cs
Normal file
102
Dotnet/AppApi/Electron/AppApiVrElectron.cs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace VRCX
|
||||||
|
{
|
||||||
|
public class AppApiVrElectron : AppApiVr
|
||||||
|
{
|
||||||
|
static AppApiVrElectron()
|
||||||
|
{
|
||||||
|
Instance = new AppApiVrElectron();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Init()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void VrInit()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||||
|
{
|
||||||
|
var list = new List<KeyValuePair<string, string>>();
|
||||||
|
while (Program.VRCXVRInstance.GetExecuteVrFeedFunctionQueue().TryDequeue(out var item))
|
||||||
|
{
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override List<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||||
|
{
|
||||||
|
var list = new List<KeyValuePair<string, string>>();
|
||||||
|
while (Program.VRCXVRInstance.GetExecuteVrOverlayFunctionQueue().TryDequeue(out var item))
|
||||||
|
{
|
||||||
|
list.Add(item);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ToggleSystemMonitor(bool enabled)
|
||||||
|
{
|
||||||
|
SystemMonitorElectron.Instance.Start(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the current CPU usage as a percentage.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current CPU usage as a percentage.</returns>
|
||||||
|
public override float CpuUsage()
|
||||||
|
{
|
||||||
|
return SystemMonitorElectron.Instance.CpuUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an array of arrays containing information about the connected VR devices.
|
||||||
|
/// Each sub-array contains the type of device and its current state
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
|
||||||
|
public override string[][] GetVRDevices()
|
||||||
|
{
|
||||||
|
return Program.VRCXVRInstance.GetDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the number of milliseconds that the system has been running.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The number of milliseconds that the system has been running.</returns>
|
||||||
|
public override double GetUptime()
|
||||||
|
{
|
||||||
|
return SystemMonitorElectron.Instance.UpTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the current language of the operating system.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The current language of the operating system.</returns>
|
||||||
|
public override string CurrentCulture()
|
||||||
|
{
|
||||||
|
return CultureInfo.CurrentCulture.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the file path of the custom user js file, if it exists.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||||
|
public override string CustomVrScript()
|
||||||
|
{
|
||||||
|
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
return File.ReadAllText(filePath);
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool IsRunningUnderWine()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,8 +45,7 @@ namespace VRCX
|
|||||||
),
|
),
|
||||||
IsLocal = true
|
IsLocal = true
|
||||||
});
|
});
|
||||||
|
|
||||||
// cefSettings.CefCommandLineArgs.Add("allow-universal-access-from-files");
|
|
||||||
// cefSettings.CefCommandLineArgs.Add("ignore-certificate-errors");
|
// cefSettings.CefCommandLineArgs.Add("ignore-certificate-errors");
|
||||||
// cefSettings.CefCommandLineArgs.Add("disable-plugins");
|
// cefSettings.CefCommandLineArgs.Add("disable-plugins");
|
||||||
cefSettings.CefCommandLineArgs.Add("disable-spell-checking");
|
cefSettings.CefCommandLineArgs.Add("disable-spell-checking");
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace VRCX
|
|||||||
repository.Register("Discord", Discord.Instance);
|
repository.Register("Discord", Discord.Instance);
|
||||||
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
|
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
|
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
|
||||||
{
|
{
|
||||||
repository.NameConverter = null;
|
repository.NameConverter = null;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ using CefSharp;
|
|||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
{
|
{
|
||||||
internal class IPCClient
|
public class IPCClient
|
||||||
{
|
{
|
||||||
private static readonly UTF8Encoding noBomEncoding = new UTF8Encoding(false, false);
|
private static readonly UTF8Encoding noBomEncoding = new UTF8Encoding(false, false);
|
||||||
private readonly NamedPipeServerStream _ipcServer;
|
private readonly NamedPipeServerStream _ipcServer;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
{
|
{
|
||||||
internal class IPCServer
|
public class IPCServer
|
||||||
{
|
{
|
||||||
public static readonly IPCServer Instance;
|
public static readonly IPCServer Instance;
|
||||||
public static readonly List<IPCClient> Clients = new List<IPCClient>();
|
public static readonly List<IPCClient> Clients = new List<IPCClient>();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,9 @@ using NLog;
|
|||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
{
|
{
|
||||||
public class SystemMonitor
|
public class SystemMonitorCef
|
||||||
{
|
{
|
||||||
public static readonly SystemMonitor Instance;
|
public static readonly SystemMonitorCef Instance;
|
||||||
public float CpuUsage;
|
public float CpuUsage;
|
||||||
public double UpTime;
|
public double UpTime;
|
||||||
private bool _enabled;
|
private bool _enabled;
|
||||||
@@ -22,9 +22,9 @@ namespace VRCX
|
|||||||
private Thread _thread;
|
private Thread _thread;
|
||||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
static SystemMonitor()
|
static SystemMonitorCef()
|
||||||
{
|
{
|
||||||
Instance = new SystemMonitor();
|
Instance = new SystemMonitorCef();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start(bool enabled)
|
public void Start(bool enabled)
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -24,7 +25,7 @@ using Device4 = SharpDX.Direct3D11.Device4;
|
|||||||
|
|
||||||
namespace VRCX
|
namespace VRCX
|
||||||
{
|
{
|
||||||
public class VRCXVR : VRCXVRInterface
|
public class VRCXVRCef : VRCXVRInterface
|
||||||
{
|
{
|
||||||
public static VRCXVRInterface Instance;
|
public static VRCXVRInterface Instance;
|
||||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
@@ -57,12 +58,12 @@ namespace VRCX
|
|||||||
private bool _wristOverlayWasActive;
|
private bool _wristOverlayWasActive;
|
||||||
|
|
||||||
|
|
||||||
static VRCXVR()
|
static VRCXVRCef()
|
||||||
{
|
{
|
||||||
Instance = new VRCXVR();
|
Instance = new VRCXVRCef();
|
||||||
}
|
}
|
||||||
|
|
||||||
public VRCXVR()
|
public VRCXVRCef()
|
||||||
{
|
{
|
||||||
_deviceListLock = new ReaderWriterLockSlim();
|
_deviceListLock = new ReaderWriterLockSlim();
|
||||||
_deviceList = new List<string[]>();
|
_deviceList = new List<string[]>();
|
||||||
@@ -90,7 +91,7 @@ namespace VRCX
|
|||||||
public override void Restart()
|
public override void Restart()
|
||||||
{
|
{
|
||||||
Exit();
|
Exit();
|
||||||
Instance = new VRCXVR();
|
Instance = new VRCXVRCef();
|
||||||
Instance.Init();
|
Instance.Init();
|
||||||
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
||||||
}
|
}
|
||||||
@@ -834,6 +835,11 @@ namespace VRCX
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public override void ExecuteVrFeedFunction(string function, string json)
|
public override void ExecuteVrFeedFunction(string function, string json)
|
||||||
{
|
{
|
||||||
if (_wristOverlay == null) return;
|
if (_wristOverlay == null) return;
|
||||||
@@ -842,6 +848,11 @@ namespace VRCX
|
|||||||
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||||
{
|
{
|
||||||
if (_hmdOverlay == null) return;
|
if (_hmdOverlay == null) return;
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -761,6 +762,11 @@ namespace VRCX
|
|||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public override void ExecuteVrFeedFunction(string function, string json)
|
public override void ExecuteVrFeedFunction(string function, string json)
|
||||||
{
|
{
|
||||||
@@ -769,6 +775,11 @@ namespace VRCX
|
|||||||
Restart();
|
Restart();
|
||||||
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||||
{
|
{
|
||||||
204
Dotnet/Overlay/Electron/GLContextWayland.cs
Normal file
204
Dotnet/Overlay/Electron/GLContextWayland.cs
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public static class GLContextWayland
|
||||||
|
{
|
||||||
|
// Wayland
|
||||||
|
[DllImport("libwayland-client.so.0")]
|
||||||
|
private static extern IntPtr wl_display_connect(string name);
|
||||||
|
|
||||||
|
[DllImport("libwayland-client.so.0")]
|
||||||
|
private static extern void wl_display_disconnect(IntPtr display);
|
||||||
|
|
||||||
|
[DllImport("libwayland-client.so.0")]
|
||||||
|
private static extern int wl_display_roundtrip(IntPtr display);
|
||||||
|
|
||||||
|
// EGL
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern IntPtr eglGetDisplay(IntPtr display_id);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern bool eglInitialize(IntPtr display, out int major, out int minor);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern bool eglTerminate(IntPtr display);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern bool eglChooseConfig(IntPtr display, int[] attrib_list, IntPtr[] configs, int config_size, out int num_config);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern IntPtr eglCreatePbufferSurface(IntPtr display, IntPtr config, int[] attrib_list);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern IntPtr eglCreateContext(IntPtr display, IntPtr config, IntPtr share_context, int[] attrib_list);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern bool eglMakeCurrent(IntPtr display, IntPtr draw, IntPtr read, IntPtr context);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern bool eglDestroySurface(IntPtr display, IntPtr surface);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern bool eglDestroyContext(IntPtr display, IntPtr context);
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern int eglGetError();
|
||||||
|
|
||||||
|
[DllImport("libEGL.so.1")]
|
||||||
|
private static extern IntPtr eglQueryString(IntPtr display, int name);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1")]
|
||||||
|
private static extern IntPtr glGetString(int name);
|
||||||
|
|
||||||
|
// EGL constants
|
||||||
|
private const int EGL_SURFACE_TYPE = 0x3033;
|
||||||
|
private const int EGL_PBUFFER_BIT = 0x0001;
|
||||||
|
private const int EGL_RENDERABLE_TYPE = 0x3040;
|
||||||
|
private const int EGL_OPENGL_BIT = 0x0008;
|
||||||
|
private const int EGL_OPENGL_ES2_BIT = 0x0004;
|
||||||
|
private const int EGL_RED_SIZE = 0x3024;
|
||||||
|
private const int EGL_GREEN_SIZE = 0x3023;
|
||||||
|
private const int EGL_BLUE_SIZE = 0x3022;
|
||||||
|
private const int EGL_ALPHA_SIZE = 0x3021;
|
||||||
|
private const int EGL_DEPTH_SIZE = 0x3025;
|
||||||
|
private const int EGL_NONE = 0x3038;
|
||||||
|
private const int EGL_WIDTH = 0x3057;
|
||||||
|
private const int EGL_HEIGHT = 0x3056;
|
||||||
|
private const int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
|
||||||
|
private const int EGL_VENDOR = 0x3053;
|
||||||
|
private const int EGL_VERSION = 0x3054;
|
||||||
|
private const int EGL_EXTENSIONS = 0x3055;
|
||||||
|
|
||||||
|
// OpenGL constants
|
||||||
|
private const int GL_VENDOR = 0x1F00;
|
||||||
|
private const int GL_RENDERER = 0x1F01;
|
||||||
|
private const int GL_VERSION = 0x1F02;
|
||||||
|
|
||||||
|
private static IntPtr waylandDisplay = IntPtr.Zero;
|
||||||
|
private static IntPtr eglDisplay = IntPtr.Zero;
|
||||||
|
private static IntPtr eglContext = IntPtr.Zero;
|
||||||
|
private static IntPtr eglSurface = IntPtr.Zero;
|
||||||
|
|
||||||
|
public static bool Initialise()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Connect to Wayland display
|
||||||
|
waylandDisplay = wl_display_connect(null);
|
||||||
|
if (waylandDisplay == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to connect to Wayland display");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform initial roundtrip to ensure connection is established
|
||||||
|
wl_display_roundtrip(waylandDisplay);
|
||||||
|
|
||||||
|
// Get EGL display
|
||||||
|
eglDisplay = eglGetDisplay(waylandDisplay);
|
||||||
|
if (eglDisplay == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to get EGL display");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize EGL
|
||||||
|
if (!eglInitialize(eglDisplay, out int major, out int minor))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to initialize EGL. Error: 0x{eglGetError():X}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"EGL version: {major}.{minor}");
|
||||||
|
|
||||||
|
// Choose EGL config for offscreen rendering
|
||||||
|
int[] configAttribs = {
|
||||||
|
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
|
||||||
|
EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT,
|
||||||
|
EGL_RED_SIZE, 8,
|
||||||
|
EGL_GREEN_SIZE, 8,
|
||||||
|
EGL_BLUE_SIZE, 8,
|
||||||
|
EGL_ALPHA_SIZE, 8,
|
||||||
|
EGL_DEPTH_SIZE, 24,
|
||||||
|
EGL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
IntPtr[] configs = new IntPtr[10];
|
||||||
|
if (!eglChooseConfig(eglDisplay, configAttribs, configs, 10, out int numConfigs) || numConfigs == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to choose EGL config. Error: 0x{eglGetError():X}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Found {numConfigs} EGL configs");
|
||||||
|
IntPtr config = configs[0];
|
||||||
|
|
||||||
|
// Create a minimal pbuffer surface (offscreen)
|
||||||
|
int[] pbufferAttribs = {
|
||||||
|
EGL_WIDTH, 1,
|
||||||
|
EGL_HEIGHT, 1,
|
||||||
|
EGL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
eglSurface = eglCreatePbufferSurface(eglDisplay, config, pbufferAttribs);
|
||||||
|
if (eglSurface == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to create EGL pbuffer surface. Error: 0x{eglGetError():X}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create OpenGL context
|
||||||
|
int[] contextAttribs = {
|
||||||
|
EGL_NONE
|
||||||
|
};
|
||||||
|
|
||||||
|
eglContext = eglCreateContext(eglDisplay, config, IntPtr.Zero, contextAttribs);
|
||||||
|
if (eglContext == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to create EGL context. Error: 0x{eglGetError():X}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make context current
|
||||||
|
if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to make EGL context current. Error: 0x{eglGetError():X}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("OpenGL context created successfully (Wayland/EGL offscreen)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to initialize OpenGL context: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Cleanup()
|
||||||
|
{
|
||||||
|
if (eglDisplay != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
if (eglContext != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
eglMakeCurrent(eglDisplay, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
||||||
|
eglDestroyContext(eglDisplay, eglContext);
|
||||||
|
}
|
||||||
|
if (eglSurface != IntPtr.Zero)
|
||||||
|
eglDestroySurface(eglDisplay, eglSurface);
|
||||||
|
|
||||||
|
eglTerminate(eglDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waylandDisplay != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
wl_display_disconnect(waylandDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
waylandDisplay = IntPtr.Zero;
|
||||||
|
eglDisplay = IntPtr.Zero;
|
||||||
|
eglContext = IntPtr.Zero;
|
||||||
|
eglSurface = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
Dotnet/Overlay/Electron/GLContextX11.cs
Normal file
170
Dotnet/Overlay/Electron/GLContextX11.cs
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
public static class GLContextX11
|
||||||
|
{
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern IntPtr XOpenDisplay(IntPtr display);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XDefaultScreen(IntPtr display);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1")]
|
||||||
|
private static extern IntPtr glXChooseVisual(IntPtr display, int screen, int[] attribList);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern IntPtr XCreateSimpleWindow(IntPtr display, IntPtr parent, int x, int y, uint width, uint height, uint border_width, ulong border, ulong background);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XMapWindow(IntPtr display, IntPtr window);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XUnmapWindow(IntPtr display, IntPtr window);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1")]
|
||||||
|
private static extern IntPtr glXCreateContext(IntPtr display, IntPtr visual, IntPtr shareList, bool direct);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1")]
|
||||||
|
private static extern bool glXMakeCurrent(IntPtr display, IntPtr drawable, IntPtr context);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1")]
|
||||||
|
private static extern void glXDestroyContext(IntPtr display, IntPtr context);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XDestroyWindow(IntPtr display, IntPtr window);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XCloseDisplay(IntPtr display);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XFlush(IntPtr display);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern IntPtr XRootWindow(IntPtr display, int screen);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XStoreName(IntPtr display, IntPtr window, string window_name);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XChangeWindowAttributes(IntPtr display, IntPtr window, ulong valuemask, ref XSetWindowAttributes attributes);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XMoveWindow(IntPtr display, IntPtr window, int x, int y);
|
||||||
|
|
||||||
|
[DllImport("libX11.so.6")]
|
||||||
|
private static extern int XResizeWindow(IntPtr display, IntPtr window, uint width, uint height);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct XSetWindowAttributes
|
||||||
|
{
|
||||||
|
public IntPtr background_pixmap;
|
||||||
|
public ulong background_pixel;
|
||||||
|
public IntPtr border_pixmap;
|
||||||
|
public ulong border_pixel;
|
||||||
|
public int bit_gravity;
|
||||||
|
public int win_gravity;
|
||||||
|
public int backing_store;
|
||||||
|
public ulong backing_planes;
|
||||||
|
public ulong backing_pixel;
|
||||||
|
public bool save_under;
|
||||||
|
public long event_mask;
|
||||||
|
public long do_not_propagate_mask;
|
||||||
|
public bool override_redirect;
|
||||||
|
public IntPtr colormap;
|
||||||
|
public IntPtr cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const ulong CWOverrideRedirect = 0x00000200;
|
||||||
|
private const ulong CWBackPixel = 0x00000002;
|
||||||
|
private const ulong CWBorderPixel = 0x00000008;
|
||||||
|
|
||||||
|
private static IntPtr display = IntPtr.Zero;
|
||||||
|
private static IntPtr window = IntPtr.Zero;
|
||||||
|
private static IntPtr context = IntPtr.Zero;
|
||||||
|
private static bool windowVisible = false;
|
||||||
|
|
||||||
|
public static bool Initialise()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
display = XOpenDisplay(IntPtr.Zero);
|
||||||
|
if (display == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("No X11 display available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int screen = XDefaultScreen(display);
|
||||||
|
|
||||||
|
// Request a visual that supports OpenGL
|
||||||
|
int[] attribs = { 4, 1, 5, 1, 12, 1, 0 }; // GLX_RGBA, GLX_DOUBLEBUFFER, GLX_DEPTH_SIZE, 1, None
|
||||||
|
IntPtr visual = glXChooseVisual(display, screen, attribs);
|
||||||
|
if (visual == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to find a GLX visual");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IntPtr root = XRootWindow(display, screen);
|
||||||
|
|
||||||
|
// Create window off-screen and very small to minimize visibility
|
||||||
|
window = XCreateSimpleWindow(display, root, -10000, -10000, 1, 1, 0, 0, 0);
|
||||||
|
if (window == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to create X11 window");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set window attributes to make it invisible
|
||||||
|
XSetWindowAttributes attrs = new XSetWindowAttributes();
|
||||||
|
attrs.override_redirect = true; // Bypass window manager
|
||||||
|
attrs.background_pixel = 0;
|
||||||
|
attrs.border_pixel = 0;
|
||||||
|
|
||||||
|
XChangeWindowAttributes(display, window, CWOverrideRedirect | CWBackPixel | CWBorderPixel, ref attrs);
|
||||||
|
|
||||||
|
// Give it a name that indicates it's a hidden OpenGL context
|
||||||
|
XStoreName(display, window, "Hidden OpenGL Context");
|
||||||
|
|
||||||
|
context = glXCreateContext(display, visual, IntPtr.Zero, true);
|
||||||
|
if (context == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to create GLX context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!glXMakeCurrent(display, window, context))
|
||||||
|
{
|
||||||
|
Console.WriteLine("Failed to make GLX context current");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't map the window at all - keep it completely hidden
|
||||||
|
// The GLX context will work without the window being visible
|
||||||
|
XFlush(display);
|
||||||
|
|
||||||
|
Console.WriteLine("OpenGL context created successfully (X11/GLX)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to initialize OpenGL context: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Cleanup()
|
||||||
|
{
|
||||||
|
if (display != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
if (context != IntPtr.Zero)
|
||||||
|
glXDestroyContext(display, context);
|
||||||
|
if (window != IntPtr.Zero)
|
||||||
|
XDestroyWindow(display, window);
|
||||||
|
XCloseDisplay(display);
|
||||||
|
display = IntPtr.Zero;
|
||||||
|
window = IntPtr.Zero;
|
||||||
|
context = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
106
Dotnet/Overlay/Electron/GLTextureWriter.cs
Normal file
106
Dotnet/Overlay/Electron/GLTextureWriter.cs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Valve.VR;
|
||||||
|
|
||||||
|
public class GLTextureWriter : IDisposable
|
||||||
|
{
|
||||||
|
private uint _textureId;
|
||||||
|
private readonly int _width, _height;
|
||||||
|
private byte[] _buffer;
|
||||||
|
|
||||||
|
// OpenGL P/Invoke declarations
|
||||||
|
[DllImport("libGL.so.1", EntryPoint = "glGenTextures")]
|
||||||
|
private static extern void glGenTextures(int n, out uint textures);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1", EntryPoint = "glBindTexture")]
|
||||||
|
private static extern void glBindTexture(uint target, uint texture);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1", EntryPoint = "glTexParameteri")]
|
||||||
|
private static extern void glTexParameteri(uint target, uint pname, int param);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1", EntryPoint = "glTexImage2D")]
|
||||||
|
private static extern void glTexImage2D(uint target, int level, int internalformat, int width, int height, int border, uint format, uint type, IntPtr pixels);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1", EntryPoint = "glTexSubImage2D")]
|
||||||
|
private static extern void glTexSubImage2D(uint target, int level, int xoffset, int yoffset, int width, int height, uint format, uint type, byte[] pixels);
|
||||||
|
|
||||||
|
[DllImport("libGL.so.1", EntryPoint = "glDeleteTextures")]
|
||||||
|
private static extern void glDeleteTextures(int n, ref uint textures);
|
||||||
|
|
||||||
|
// OpenGL constants
|
||||||
|
private const uint GL_TEXTURE_2D = 0x0DE1;
|
||||||
|
private const uint GL_TEXTURE_MIN_FILTER = 0x2801;
|
||||||
|
private const uint GL_TEXTURE_MAG_FILTER = 0x2800;
|
||||||
|
private const uint GL_TEXTURE_WRAP_S = 0x2802;
|
||||||
|
private const uint GL_TEXTURE_WRAP_T = 0x2803;
|
||||||
|
private const uint GL_LINEAR = 0x2601;
|
||||||
|
private const uint GL_CLAMP_TO_EDGE = 0x812F;
|
||||||
|
private const uint GL_RGBA = 0x1908;
|
||||||
|
private const uint GL_BGRA = 0x80E1;
|
||||||
|
private const uint GL_UNSIGNED_BYTE = 0x1401;
|
||||||
|
|
||||||
|
public GLTextureWriter(int width, int height)
|
||||||
|
{
|
||||||
|
_width = width;
|
||||||
|
_height = height;
|
||||||
|
_buffer = new byte[width * height * 4]; // 4 bytes per pixel (RGBA)
|
||||||
|
InitTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitTexture()
|
||||||
|
{
|
||||||
|
glGenTextures(1, out _textureId);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, _textureId);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, (int)GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, (int)GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (int)GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (int)GL_CLAMP_TO_EDGE);
|
||||||
|
|
||||||
|
// Allocate texture storage with no initial data.
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, (int)GL_RGBA, _width, _height, 0,
|
||||||
|
GL_BGRA, GL_UNSIGNED_BYTE, IntPtr.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies image data into the internal buffer.
|
||||||
|
/// </summary>
|
||||||
|
public void WriteImageToBuffer(byte[] data)
|
||||||
|
{
|
||||||
|
if (data.Length != _buffer.Length)
|
||||||
|
throw new ArgumentException("Data size does not match texture size.");
|
||||||
|
|
||||||
|
System.Buffer.BlockCopy(data, 0, _buffer, 0, _buffer.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the OpenGL texture with the current buffer.
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateTexture()
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, _textureId);
|
||||||
|
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, _width, _height,
|
||||||
|
GL_BGRA, GL_UNSIGNED_BYTE, _buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a Texture_t structure for use with OpenVR.
|
||||||
|
/// </summary>
|
||||||
|
public Texture_t AsTextureT()
|
||||||
|
{
|
||||||
|
return new Texture_t
|
||||||
|
{
|
||||||
|
handle = (IntPtr)_textureId,
|
||||||
|
eType = ETextureType.OpenGL,
|
||||||
|
eColorSpace = EColorSpace.Auto
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_textureId != 0)
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, ref _textureId);
|
||||||
|
_textureId = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
214
Dotnet/Overlay/Electron/SystemMonitorElectron.cs
Normal file
214
Dotnet/Overlay/Electron/SystemMonitorElectron.cs
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// This work is licensed under the terms of the MIT license.
|
||||||
|
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using NLog;
|
||||||
|
|
||||||
|
namespace VRCX
|
||||||
|
{
|
||||||
|
public class SystemMonitorElectron
|
||||||
|
{
|
||||||
|
public static readonly SystemMonitorElectron Instance;
|
||||||
|
public float CpuUsage;
|
||||||
|
public double UpTime;
|
||||||
|
private bool _enabled;
|
||||||
|
private long _lastTotalTime;
|
||||||
|
private long _lastIdleTime;
|
||||||
|
private bool _firstReading = true; // Add this flag
|
||||||
|
private Thread _thread;
|
||||||
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
|
|
||||||
|
static SystemMonitorElectron()
|
||||||
|
{
|
||||||
|
Instance = new SystemMonitorElectron();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Start(bool enabled)
|
||||||
|
{
|
||||||
|
if (enabled == _enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_enabled = enabled;
|
||||||
|
if (enabled)
|
||||||
|
StartThread();
|
||||||
|
else
|
||||||
|
Exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Exit()
|
||||||
|
{
|
||||||
|
CpuUsage = 0;
|
||||||
|
UpTime = 0;
|
||||||
|
_firstReading = true; // Reset flag
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_thread != null)
|
||||||
|
{
|
||||||
|
_thread.Interrupt();
|
||||||
|
_thread.Join();
|
||||||
|
_thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (ThreadInterruptedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartThread()
|
||||||
|
{
|
||||||
|
Exit();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ReadCpuInfo();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error($"Failed to initialize CPU monitoring: {ex}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_thread = new Thread(ThreadProc)
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThreadProc()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (_enabled)
|
||||||
|
{
|
||||||
|
UpdateMetrics();
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Warn($"SystemMonitor thread exception: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMetrics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CpuUsage = GetCpuUsage();
|
||||||
|
UpTime = GetUptime();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Warn($"Error updating metrics: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float GetCpuUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cpuInfo = ReadCpuInfo();
|
||||||
|
if (cpuInfo == null) return 0;
|
||||||
|
|
||||||
|
var totalTime = cpuInfo.Value.TotalTime;
|
||||||
|
var idleTime = cpuInfo.Value.IdleTime;
|
||||||
|
|
||||||
|
// Skip the first reading to establish baseline
|
||||||
|
if (_firstReading)
|
||||||
|
{
|
||||||
|
_lastTotalTime = totalTime;
|
||||||
|
_lastIdleTime = idleTime;
|
||||||
|
_firstReading = false;
|
||||||
|
return 0; // Return 0 for first reading
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalTimeDiff = totalTime - _lastTotalTime;
|
||||||
|
var idleTimeDiff = idleTime - _lastIdleTime;
|
||||||
|
|
||||||
|
if (totalTimeDiff > 0)
|
||||||
|
{
|
||||||
|
var cpuUsage = 100.0f * (1.0f - (float)idleTimeDiff / totalTimeDiff);
|
||||||
|
_lastTotalTime = totalTime;
|
||||||
|
_lastIdleTime = idleTime;
|
||||||
|
|
||||||
|
// Clamp to reasonable range
|
||||||
|
return Math.Max(0, Math.Min(100, cpuUsage));
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Warn($"Error reading CPU usage: {ex}");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetUptime()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uptimeContent = File.ReadAllText("/proc/uptime");
|
||||||
|
var parts = uptimeContent.Split(' ');
|
||||||
|
if (parts.Length > 0 && double.TryParse(parts[0], out var uptimeSeconds))
|
||||||
|
{
|
||||||
|
return TimeSpan.FromSeconds(uptimeSeconds).TotalMilliseconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Warn($"Error reading uptime: {ex}");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (long TotalTime, long IdleTime)? ReadCpuInfo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var statContent = File.ReadAllText("/proc/stat");
|
||||||
|
var lines = statContent.Split('\n');
|
||||||
|
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (line.StartsWith("cpu "))
|
||||||
|
{
|
||||||
|
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
if (parts.Length >= 5)
|
||||||
|
{
|
||||||
|
// CPU time values: user, nice, system, idle, iowait, irq, softirq, steal, guest, guest_nice
|
||||||
|
var user = long.Parse(parts[1]);
|
||||||
|
var nice = long.Parse(parts[2]);
|
||||||
|
var system = long.Parse(parts[3]);
|
||||||
|
var idle = long.Parse(parts[4]);
|
||||||
|
var iowait = parts.Length > 5 ? long.Parse(parts[5]) : 0;
|
||||||
|
var irq = parts.Length > 6 ? long.Parse(parts[6]) : 0;
|
||||||
|
var softirq = parts.Length > 7 ? long.Parse(parts[7]) : 0;
|
||||||
|
var steal = parts.Length > 8 ? long.Parse(parts[8]) : 0;
|
||||||
|
|
||||||
|
var totalTime = user + nice + system + idle + iowait + irq + softirq + steal;
|
||||||
|
return (totalTime, idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Warn($"Error reading /proc/stat: {ex}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
921
Dotnet/Overlay/Electron/VRCXVRElectron.cs
Normal file
921
Dotnet/Overlay/Electron/VRCXVRElectron.cs
Normal file
@@ -0,0 +1,921 @@
|
|||||||
|
// Copyright(c) 2019-2025 pypy, Natsumi and individual contributors.
|
||||||
|
// All rights reserved.
|
||||||
|
//
|
||||||
|
// This work is licensed under the terms of the MIT license.
|
||||||
|
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using NLog;
|
||||||
|
using Valve.VR;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.IO.MemoryMappedFiles;
|
||||||
|
|
||||||
|
namespace VRCX
|
||||||
|
{
|
||||||
|
public class VRCXVRElectron : VRCXVRInterface
|
||||||
|
{
|
||||||
|
public static VRCXVRInterface Instance;
|
||||||
|
private static readonly Logger logger = 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) };
|
||||||
|
private readonly List<string[]> _deviceList;
|
||||||
|
private readonly ReaderWriterLockSlim _deviceListLock;
|
||||||
|
private bool _active;
|
||||||
|
private bool _menuButton;
|
||||||
|
private int _overlayHand;
|
||||||
|
private GLTextureWriter _wristOverlayTextureWriter;
|
||||||
|
private GLTextureWriter _hmdOverlayTextureWriter;
|
||||||
|
private Thread _thread;
|
||||||
|
private DateTime _nextOverlayUpdate;
|
||||||
|
|
||||||
|
private ulong _hmdOverlayHandle;
|
||||||
|
private bool _hmdOverlayActive;
|
||||||
|
private bool _hmdOverlayWasActive;
|
||||||
|
|
||||||
|
private ulong _wristOverlayHandle;
|
||||||
|
private bool _wristOverlayActive;
|
||||||
|
private bool _wristOverlayWasActive;
|
||||||
|
|
||||||
|
private const string WRIST_OVERLAY_SHM_PATH = "/dev/shm/vrcx_wrist_overlay";
|
||||||
|
private const string HMD_OVERLAY_SHM_PATH = "/dev/shm/vrcx_hmd_overlay";
|
||||||
|
private const int WRIST_FRAME_WIDTH = 512;
|
||||||
|
private const int WRIST_FRAME_HEIGHT = 512;
|
||||||
|
private const int WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4; // RGBA
|
||||||
|
private byte[] wristFrameBuffer = new byte[WRIST_FRAME_SIZE];
|
||||||
|
private const int HMD_FRAME_WIDTH = 1024;
|
||||||
|
private const int HMD_FRAME_HEIGHT = 1024;
|
||||||
|
private const int HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4; // RGBA
|
||||||
|
private byte[] hmdFrameBuffer = new byte[HMD_FRAME_SIZE];
|
||||||
|
private MemoryMappedFile _wristOverlayMMF;
|
||||||
|
private MemoryMappedViewAccessor _wristOverlayAccessor;
|
||||||
|
private MemoryMappedFile _hmdOverlayMMF;
|
||||||
|
private MemoryMappedViewAccessor _hmdOverlayAccessor;
|
||||||
|
private readonly ConcurrentQueue<KeyValuePair<string, string>> _wristFeedFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
|
||||||
|
private readonly ConcurrentQueue<KeyValuePair<string, string>> _hmdFeedFunctionQueue = new ConcurrentQueue<KeyValuePair<string, string>>();
|
||||||
|
|
||||||
|
static VRCXVRElectron()
|
||||||
|
{
|
||||||
|
Instance = new VRCXVRElectron();
|
||||||
|
}
|
||||||
|
|
||||||
|
public VRCXVRElectron()
|
||||||
|
{
|
||||||
|
_deviceListLock = new ReaderWriterLockSlim();
|
||||||
|
_deviceList = new List<string[]>();
|
||||||
|
_thread = new Thread(ThreadLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE
|
||||||
|
// 메모리 릭 때문에 미리 생성해놓고 계속 사용함
|
||||||
|
public override void Init()
|
||||||
|
{
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Exit()
|
||||||
|
{
|
||||||
|
var thread = _thread;
|
||||||
|
_thread = null;
|
||||||
|
thread?.Interrupt();
|
||||||
|
thread?.Join();
|
||||||
|
|
||||||
|
_wristOverlayAccessor?.Dispose();
|
||||||
|
_wristOverlayAccessor = null;
|
||||||
|
_wristOverlayMMF?.Dispose();
|
||||||
|
_wristOverlayMMF = null;
|
||||||
|
|
||||||
|
_hmdOverlayAccessor?.Dispose();
|
||||||
|
_hmdOverlayAccessor = null;
|
||||||
|
_hmdOverlayMMF?.Dispose();
|
||||||
|
_hmdOverlayMMF = null;
|
||||||
|
|
||||||
|
GLContextX11.Cleanup();
|
||||||
|
GLContextWayland.Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Restart()
|
||||||
|
{
|
||||||
|
Exit();
|
||||||
|
Instance = new VRCXVRElectron();
|
||||||
|
Instance.Init();
|
||||||
|
//MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupTextures()
|
||||||
|
{
|
||||||
|
bool contextInitialised = false;
|
||||||
|
|
||||||
|
// Check if we're running on Wayland
|
||||||
|
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")) ||
|
||||||
|
Environment.GetEnvironmentVariable("XDG_SESSION_TYPE")?.ToLower() == "wayland")
|
||||||
|
{
|
||||||
|
contextInitialised = GLContextWayland.Initialise();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contextInitialised = GLContextX11.Initialise();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextInitialised)
|
||||||
|
{
|
||||||
|
contextInitialised = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("WAYLAND_DISPLAY")) ?
|
||||||
|
GLContextX11.Initialise() : GLContextWayland.Initialise();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contextInitialised)
|
||||||
|
{
|
||||||
|
throw new Exception("Failed to initialise OpenGL context");
|
||||||
|
}
|
||||||
|
|
||||||
|
_wristOverlayTextureWriter = new GLTextureWriter(512, 512);
|
||||||
|
_wristOverlayTextureWriter.UpdateTexture();
|
||||||
|
|
||||||
|
_hmdOverlayTextureWriter = new GLTextureWriter(1024, 1024);
|
||||||
|
_hmdOverlayTextureWriter.UpdateTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpgradeDevice()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] GetLatestWristOverlayFrame()
|
||||||
|
{
|
||||||
|
if (_wristOverlayAccessor == null) return null;
|
||||||
|
byte ready = _wristOverlayAccessor.ReadByte(0);
|
||||||
|
if (ready == 1)
|
||||||
|
{
|
||||||
|
_wristOverlayAccessor.ReadArray(1, wristFrameBuffer, 0, WRIST_FRAME_SIZE);
|
||||||
|
_wristOverlayAccessor.Write(0, (byte)0); // reset flag
|
||||||
|
return wristFrameBuffer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] GetLatestHmdOverlayFrame()
|
||||||
|
{
|
||||||
|
if (_hmdOverlayAccessor == null) return null;
|
||||||
|
byte ready = _hmdOverlayAccessor.ReadByte(0);
|
||||||
|
if (ready == 1)
|
||||||
|
{
|
||||||
|
_hmdOverlayAccessor.ReadArray(1, hmdFrameBuffer, 0, HMD_FRAME_SIZE);
|
||||||
|
_hmdOverlayAccessor.Write(0, (byte)0); // reset flag
|
||||||
|
return hmdFrameBuffer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlipImageVertically(byte[] imageData, int width, int height)
|
||||||
|
{
|
||||||
|
int stride = width * 4; // 4 bytes per pixel (RGBA)
|
||||||
|
byte[] tempRow = new byte[stride];
|
||||||
|
|
||||||
|
for (int y = 0; y < height / 2; y++)
|
||||||
|
{
|
||||||
|
int topIndex = y * stride;
|
||||||
|
int bottomIndex = (height - 1 - y) * stride;
|
||||||
|
|
||||||
|
// Swap rows
|
||||||
|
Buffer.BlockCopy(imageData, topIndex, tempRow, 0, stride);
|
||||||
|
Buffer.BlockCopy(imageData, bottomIndex, imageData, topIndex, stride);
|
||||||
|
Buffer.BlockCopy(tempRow, 0, imageData, bottomIndex, stride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThreadLoop()
|
||||||
|
{
|
||||||
|
var active = false;
|
||||||
|
var e = new VREvent_t();
|
||||||
|
var nextInit = DateTime.MinValue;
|
||||||
|
var nextDeviceUpdate = DateTime.MinValue;
|
||||||
|
_nextOverlayUpdate = DateTime.MinValue;
|
||||||
|
var overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
|
||||||
|
var overlayVisible1 = false;
|
||||||
|
var overlayVisible2 = false;
|
||||||
|
var dashboardHandle = 0UL;
|
||||||
|
|
||||||
|
while (_thread != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_active)
|
||||||
|
Thread.Sleep(1);
|
||||||
|
else
|
||||||
|
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;
|
||||||
|
SetupTextures();
|
||||||
|
}
|
||||||
|
|
||||||
|
while (system.PollNextEvent(ref e, (uint)Marshal.SizeOf(e)))
|
||||||
|
{
|
||||||
|
var type = (EVREventType)e.eventType;
|
||||||
|
if (type == EVREventType.VREvent_Quit)
|
||||||
|
{
|
||||||
|
active = false;
|
||||||
|
IsHmdAfk = false;
|
||||||
|
OpenVR.Shutdown();
|
||||||
|
nextInit = DateTime.UtcNow.AddSeconds(10);
|
||||||
|
system = null;
|
||||||
|
|
||||||
|
_wristOverlayHandle = 0;
|
||||||
|
_hmdOverlayHandle = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (system != null)
|
||||||
|
{
|
||||||
|
if (DateTime.UtcNow.CompareTo(nextDeviceUpdate) >= 0)
|
||||||
|
{
|
||||||
|
overlayIndex = OpenVR.k_unTrackedDeviceIndexInvalid;
|
||||||
|
UpdateDevices(system, ref overlayIndex);
|
||||||
|
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
|
||||||
|
{
|
||||||
|
_nextOverlayUpdate = 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);
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (_wristOverlayActive)
|
||||||
|
{
|
||||||
|
var err = ProcessOverlay1(overlay, ref _wristOverlayHandle, ref overlayVisible1,
|
||||||
|
dashboardVisible, overlayIndex);
|
||||||
|
if (err != EVROverlayError.None &&
|
||||||
|
_wristOverlayHandle != 0)
|
||||||
|
{
|
||||||
|
overlay.DestroyOverlay(_wristOverlayHandle);
|
||||||
|
_wristOverlayHandle = 0;
|
||||||
|
logger.Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_hmdOverlayActive)
|
||||||
|
{
|
||||||
|
var err = ProcessOverlay2(overlay, ref _hmdOverlayHandle, ref overlayVisible2,
|
||||||
|
dashboardVisible);
|
||||||
|
if (err != EVROverlayError.None &&
|
||||||
|
_hmdOverlayHandle != 0)
|
||||||
|
{
|
||||||
|
overlay.DestroyOverlay(_hmdOverlayHandle);
|
||||||
|
_hmdOverlayHandle = 0;
|
||||||
|
logger.Error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (active)
|
||||||
|
{
|
||||||
|
active = false;
|
||||||
|
IsHmdAfk = false;
|
||||||
|
OpenVR.Shutdown();
|
||||||
|
_deviceListLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_deviceList.Clear();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_deviceListLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_wristOverlayAccessor?.Dispose();
|
||||||
|
_wristOverlayAccessor = null;
|
||||||
|
_wristOverlayMMF?.Dispose();
|
||||||
|
_wristOverlayMMF = null;
|
||||||
|
|
||||||
|
_hmdOverlayAccessor?.Dispose();
|
||||||
|
_hmdOverlayAccessor = null;
|
||||||
|
_hmdOverlayMMF?.Dispose();
|
||||||
|
_hmdOverlayMMF = null;
|
||||||
|
|
||||||
|
GLContextX11.Cleanup();
|
||||||
|
GLContextWayland.Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetActive(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||||
|
{
|
||||||
|
_active = active;
|
||||||
|
_hmdOverlayActive = hmdOverlay;
|
||||||
|
_wristOverlayActive = wristOverlay;
|
||||||
|
_menuButton = menuButton;
|
||||||
|
_overlayHand = overlayHand;
|
||||||
|
|
||||||
|
if (_hmdOverlayActive != _hmdOverlayWasActive && _hmdOverlayHandle != 0)
|
||||||
|
{
|
||||||
|
OpenVR.Overlay.DestroyOverlay(_hmdOverlayHandle);
|
||||||
|
_hmdOverlayHandle = 0;
|
||||||
|
|
||||||
|
_hmdOverlayAccessor?.Dispose();
|
||||||
|
_hmdOverlayAccessor = null;
|
||||||
|
_hmdOverlayMMF?.Dispose();
|
||||||
|
_hmdOverlayMMF = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hmdOverlayWasActive = _hmdOverlayActive;
|
||||||
|
|
||||||
|
if (_wristOverlayActive != _wristOverlayWasActive && _wristOverlayHandle != 0)
|
||||||
|
{
|
||||||
|
OpenVR.Overlay.DestroyOverlay(_wristOverlayHandle);
|
||||||
|
_wristOverlayHandle = 0;
|
||||||
|
|
||||||
|
_wristOverlayAccessor?.Dispose();
|
||||||
|
_wristOverlayAccessor = null;
|
||||||
|
_wristOverlayMMF?.Dispose();
|
||||||
|
_wristOverlayMMF = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wristOverlayWasActive = _wristOverlayActive;
|
||||||
|
|
||||||
|
if (!_active)
|
||||||
|
{
|
||||||
|
GLContextX11.Cleanup();
|
||||||
|
GLContextWayland.Cleanup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Refresh()
|
||||||
|
{
|
||||||
|
//_wristOverlay.Reload();
|
||||||
|
//_hmdOverlay.Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string[][] GetDevices()
|
||||||
|
{
|
||||||
|
_deviceListLock.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _deviceList.ToArray();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_deviceListLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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);
|
||||||
|
switch (devClass)
|
||||||
|
{
|
||||||
|
case ETrackedDeviceClass.HMD:
|
||||||
|
var success = system.GetControllerState(i, ref state, (uint)Marshal.SizeOf(state));
|
||||||
|
if (!success)
|
||||||
|
break; // this fails while SteamVR overlay is open
|
||||||
|
|
||||||
|
var prox = state.ulButtonPressed & (1UL << ((int)EVRButtonId.k_EButton_ProximitySensor));
|
||||||
|
var isHmdAfk = prox == 0;
|
||||||
|
if (isHmdAfk != IsHmdAfk)
|
||||||
|
{
|
||||||
|
IsHmdAfk = isHmdAfk;
|
||||||
|
Program.AppApiInstance.CheckGameRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
var headsetErr = ETrackedPropertyError.TrackedProp_Success;
|
||||||
|
var headsetBatteryPercentage = system.GetFloatTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_DeviceBatteryPercentage_Float, ref headsetErr);
|
||||||
|
if (headsetErr != ETrackedPropertyError.TrackedProp_Success)
|
||||||
|
{
|
||||||
|
// Headset has no battery, skip displaying it
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var headset = new[]
|
||||||
|
{
|
||||||
|
"headset",
|
||||||
|
system.IsTrackedDeviceConnected(i)
|
||||||
|
? "connected"
|
||||||
|
: "disconnected",
|
||||||
|
// Currently neither VD or SteamLink report charging state
|
||||||
|
"discharging",
|
||||||
|
(headsetBatteryPercentage * 100).ToString(),
|
||||||
|
poses[i].eTrackingResult.ToString()
|
||||||
|
};
|
||||||
|
_deviceListLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_deviceList.Add(headset);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_deviceListLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case ETrackedDeviceClass.Controller:
|
||||||
|
case ETrackedDeviceClass.GenericTracker:
|
||||||
|
case 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)
|
||||||
|
{
|
||||||
|
_nextOverlayUpdate = DateTime.MinValue;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.Join(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 = _wristOverlay.Size;
|
||||||
|
//_wristOverlay.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 = _wristOverlay.Size;
|
||||||
|
//_wristOverlay.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 = _wristOverlay.Size;
|
||||||
|
//_wristOverlay.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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_wristOverlayMMF = MemoryMappedFile.CreateFromFile(WRIST_OVERLAY_SHM_PATH, FileMode.Open, null, WRIST_FRAME_SIZE + 1);
|
||||||
|
_wristOverlayAccessor = _wristOverlayMMF.CreateViewAccessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlayIndex != OpenVR.k_unTrackedDeviceIndexInvalid)
|
||||||
|
{
|
||||||
|
// http://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices
|
||||||
|
// Scaling-Rotation-Translation
|
||||||
|
var m = Matrix4x4.CreateScale(0.25f);
|
||||||
|
m *= Matrix4x4.CreateRotationX(_rotation[0]);
|
||||||
|
m *= Matrix4x4.CreateRotationY(_rotation[1]);
|
||||||
|
m *= Matrix4x4.CreateRotationZ(_rotation[2]);
|
||||||
|
m *= Matrix4x4.CreateTranslation(new Vector3(_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(_nextOverlayUpdate) <= 0)
|
||||||
|
{
|
||||||
|
if (_wristOverlayTextureWriter != null)
|
||||||
|
{
|
||||||
|
byte[] imageData = GetLatestWristOverlayFrame();
|
||||||
|
if (imageData != null)
|
||||||
|
{
|
||||||
|
FlipImageVertically(imageData, WRIST_FRAME_WIDTH, WRIST_FRAME_HEIGHT);
|
||||||
|
_wristOverlayTextureWriter.WriteImageToBuffer(imageData);
|
||||||
|
_wristOverlayTextureWriter.UpdateTexture();
|
||||||
|
|
||||||
|
Texture_t texture = _wristOverlayTextureWriter.AsTextureT();
|
||||||
|
err = OpenVR.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 = Matrix4x4.CreateScale(1f);
|
||||||
|
m *= Matrix4x4.CreateTranslation(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hmdOverlayMMF = MemoryMappedFile.CreateFromFile(HMD_OVERLAY_SHM_PATH, FileMode.Open, null, HMD_FRAME_SIZE + 1);
|
||||||
|
_hmdOverlayAccessor = _hmdOverlayMMF.CreateViewAccessor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dashboardVisible)
|
||||||
|
{
|
||||||
|
if (_hmdOverlayTextureWriter != null)
|
||||||
|
{
|
||||||
|
byte[] imageData = GetLatestHmdOverlayFrame();
|
||||||
|
if (imageData != null)
|
||||||
|
{
|
||||||
|
FlipImageVertically(imageData, HMD_FRAME_WIDTH, HMD_FRAME_HEIGHT);
|
||||||
|
_hmdOverlayTextureWriter.WriteImageToBuffer(imageData);
|
||||||
|
_hmdOverlayTextureWriter.UpdateTexture();
|
||||||
|
|
||||||
|
Texture_t texture = _hmdOverlayTextureWriter.AsTextureT();
|
||||||
|
err = OpenVR.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
|
||||||
|
{
|
||||||
|
return _wristFeedFunctionQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ExecuteVrFeedFunction(string function, string json)
|
||||||
|
{
|
||||||
|
//if (_hmdOverlaySocket == null || !_hmdOverlaySocket.Connected) return;
|
||||||
|
// if (_wristOverlay.IsLoading)
|
||||||
|
// Restart();
|
||||||
|
|
||||||
|
_wristFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
|
||||||
|
{
|
||||||
|
return _hmdFeedFunctionQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||||
|
{
|
||||||
|
//if (_hmdOverlaySocket == null || !_hmdOverlaySocket.Connected) return;
|
||||||
|
// if (_hmdOverlay.IsLoading)
|
||||||
|
// Restart();
|
||||||
|
|
||||||
|
_hmdFeedFunctionQueue.Enqueue(new KeyValuePair<string, string>(function, json));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
namespace VRCX;
|
namespace VRCX;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
public abstract class VRCXVRInterface
|
public abstract class VRCXVRInterface
|
||||||
{
|
{
|
||||||
public bool IsHmdAfk;
|
public bool IsHmdAfk;
|
||||||
|
|
||||||
public abstract void Init();
|
public abstract void Init();
|
||||||
public abstract void Exit();
|
public abstract void Exit();
|
||||||
public abstract void Refresh();
|
public abstract void Refresh();
|
||||||
@@ -12,4 +14,6 @@ public abstract class VRCXVRInterface
|
|||||||
public abstract string[][] GetDevices();
|
public abstract string[][] GetDevices();
|
||||||
public abstract void ExecuteVrFeedFunction(string function, string json);
|
public abstract void ExecuteVrFeedFunction(string function, string json);
|
||||||
public abstract void ExecuteVrOverlayFunction(string function, string json);
|
public abstract void ExecuteVrOverlayFunction(string function, string json);
|
||||||
|
public abstract ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue();
|
||||||
|
public abstract ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue();
|
||||||
}
|
}
|
||||||
@@ -24,10 +24,9 @@ namespace VRCX
|
|||||||
public static string Version { get; private set; }
|
public static string Version { get; private set; }
|
||||||
public static bool LaunchDebug;
|
public static bool LaunchDebug;
|
||||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||||
#if !LINUX
|
|
||||||
public static VRCXVRInterface VRCXVRInstance { get; private set; }
|
public static VRCXVRInterface VRCXVRInstance { get; private set; }
|
||||||
#endif
|
|
||||||
public static AppApi AppApiInstance { get; private set; }
|
public static AppApi AppApiInstance { get; private set; }
|
||||||
|
public static AppApiVr AppApiVrInstance { get; private set; }
|
||||||
|
|
||||||
private static void SetProgramDirectories()
|
private static void SetProgramDirectories()
|
||||||
{
|
{
|
||||||
@@ -74,7 +73,7 @@ namespace VRCX
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var versionFile = File.ReadAllText(Path.Join(BaseDirectory, "Version")).Trim();
|
var versionFile = File.ReadAllText(Path.Join(BaseDirectory, "Version")).Trim();
|
||||||
|
|
||||||
// look for trailing git hash "-22bcd96" to indicate nightly build
|
// look for trailing git hash "-22bcd96" to indicate nightly build
|
||||||
var version = versionFile.Split('-');
|
var version = versionFile.Split('-');
|
||||||
if (version.Length > 0 && version[^1].Length == 7)
|
if (version.Length > 0 && version[^1].Length == 7)
|
||||||
@@ -234,7 +233,8 @@ namespace VRCX
|
|||||||
SQLiteLegacy.Instance.Init();
|
SQLiteLegacy.Instance.Init();
|
||||||
AppApiInstance = new AppApiCef();
|
AppApiInstance = new AppApiCef();
|
||||||
|
|
||||||
AppApiVr.Instance.Init();
|
AppApiVrInstance = new AppApiVrCef();
|
||||||
|
AppApiVrInstance.Init();
|
||||||
ProcessMonitor.Instance.Init();
|
ProcessMonitor.Instance.Init();
|
||||||
Discord.Instance.Init();
|
Discord.Instance.Init();
|
||||||
WebApi.Instance.Init();
|
WebApi.Instance.Init();
|
||||||
@@ -246,7 +246,7 @@ namespace VRCX
|
|||||||
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
|
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
|
||||||
VRCXVRInstance = new VRCXVRLegacy();
|
VRCXVRInstance = new VRCXVRLegacy();
|
||||||
else
|
else
|
||||||
VRCXVRInstance = new VRCXVR();
|
VRCXVRInstance = new VRCXVRCef();
|
||||||
VRCXVRInstance.Init();
|
VRCXVRInstance.Init();
|
||||||
|
|
||||||
Application.Run(new MainForm());
|
Application.Run(new MainForm());
|
||||||
@@ -260,7 +260,7 @@ namespace VRCX
|
|||||||
WebApi.Instance.Exit();
|
WebApi.Instance.Exit();
|
||||||
|
|
||||||
Discord.Instance.Exit();
|
Discord.Instance.Exit();
|
||||||
SystemMonitor.Instance.Exit();
|
SystemMonitorCef.Instance.Exit();
|
||||||
VRCXStorage.Instance.Save();
|
VRCXStorage.Instance.Save();
|
||||||
SQLiteLegacy.Instance.Exit();
|
SQLiteLegacy.Instance.Exit();
|
||||||
ProcessMonitor.Instance.Exit();
|
ProcessMonitor.Instance.Exit();
|
||||||
@@ -285,6 +285,9 @@ namespace VRCX
|
|||||||
|
|
||||||
AppApiInstance = new AppApiElectron();
|
AppApiInstance = new AppApiElectron();
|
||||||
// ProcessMonitor.Instance.Init();
|
// ProcessMonitor.Instance.Init();
|
||||||
|
|
||||||
|
VRCXVRInstance = new VRCXVRElectron();
|
||||||
|
VRCXVRInstance.Init();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,8 +121,8 @@
|
|||||||
<Compile Remove="Cef\**" />
|
<Compile Remove="Cef\**" />
|
||||||
<Content Remove="AppApi\Cef\**" />
|
<Content Remove="AppApi\Cef\**" />
|
||||||
<Compile Remove="AppApi\Cef\**" />
|
<Compile Remove="AppApi\Cef\**" />
|
||||||
<Content Remove="Overlay\**" />
|
<Content Remove="Overlay\Cef\**" />
|
||||||
<Compile Remove="Overlay\**" />
|
<Compile Remove="Overlay\Cef\**" />
|
||||||
<Content Remove="DBMerger\**" />
|
<Content Remove="DBMerger\**" />
|
||||||
<Compile Remove="DBMerger\**" />
|
<Compile Remove="DBMerger\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ namespace VRCX
|
|||||||
catch (UriFormatException)
|
catch (UriFormatException)
|
||||||
{
|
{
|
||||||
VRCXStorage.Instance.Set("VRCX_ProxyServer", string.Empty);
|
VRCXStorage.Instance.Set("VRCX_ProxyServer", string.Empty);
|
||||||
|
VRCXStorage.Instance.Flush();
|
||||||
const string message = "The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI.";
|
const string message = "The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI.";
|
||||||
#if !LINUX
|
#if !LINUX
|
||||||
System.Windows.Forms.MessageBox.Show(message, "Invalid Proxy URI", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
System.Windows.Forms.MessageBox.Show(message, "Invalid Proxy URI", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
|||||||
BIN
VRCX.png
BIN
VRCX.png
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 31 KiB |
11
package.json
11
package.json
@@ -139,10 +139,19 @@
|
|||||||
},
|
},
|
||||||
"maintainer": "rs189 <35667100+rs189@users.noreply.github.com>",
|
"maintainer": "rs189 <35667100+rs189@users.noreply.github.com>",
|
||||||
"description": "Friendship management tool for VRChat"
|
"description": "Friendship management tool for VRChat"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"artifactName": "VRCX_Version.${ext}",
|
||||||
|
"target": [
|
||||||
|
"dmg"
|
||||||
|
],
|
||||||
|
"icon": "VRCX.png",
|
||||||
|
"category": "public.app-category.utilities",
|
||||||
|
"executableName": "VRCX"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hazardous": "^0.3.0",
|
"hazardous": "^0.3.0",
|
||||||
"node-api-dotnet": "^0.9.12"
|
"node-api-dotnet": "^0.9.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,88 +5,101 @@ const { spawnSync } = require('child_process');
|
|||||||
|
|
||||||
const DOTNET_VERSION = '9.0.7';
|
const DOTNET_VERSION = '9.0.7';
|
||||||
const DOTNET_RUNTIME_URL = `https://builds.dotnet.microsoft.com/dotnet/Runtime/${DOTNET_VERSION}/dotnet-runtime-${DOTNET_VERSION}-linux-x64.tar.gz`;
|
const DOTNET_RUNTIME_URL = `https://builds.dotnet.microsoft.com/dotnet/Runtime/${DOTNET_VERSION}/dotnet-runtime-${DOTNET_VERSION}-linux-x64.tar.gz`;
|
||||||
const DOTNET_RUNTIME_DIR = path.join(__dirname, '..', 'build', 'Electron', 'dotnet-runtime');
|
const DOTNET_RUNTIME_DIR = path.join(
|
||||||
const DOTNET_BIN_DIR = path.join(DOTNET_RUNTIME_DIR, 'bin');
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'build',
|
||||||
|
'Electron',
|
||||||
|
'dotnet-runtime'
|
||||||
|
);
|
||||||
|
|
||||||
async function downloadFile(url, targetPath) {
|
async function downloadFile(url, targetPath) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const file = fs.createWriteStream(targetPath);
|
const file = fs.createWriteStream(targetPath);
|
||||||
https.get(url, (response) => {
|
https
|
||||||
if (response.statusCode !== 200) {
|
.get(url, (response) => {
|
||||||
reject(new Error(`Failed to download, status code: ${response.statusCode}`));
|
if (response.statusCode !== 200) {
|
||||||
return;
|
reject(
|
||||||
}
|
new Error(
|
||||||
response.pipe(file);
|
`Failed to download, status code: ${response.statusCode}`
|
||||||
file.on('finish', () => {
|
)
|
||||||
file.close(resolve);
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close(resolve);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.on('error', (err) => {
|
||||||
|
fs.unlink(targetPath, () => reject(err));
|
||||||
});
|
});
|
||||||
}).on('error', (err) => {
|
|
||||||
fs.unlink(targetPath, () => reject(err));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extractTarGz(tarGzPath, extractDir) {
|
async function extractTarGz(tarGzPath, extractDir) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const tar = spawnSync('tar', ['-xzf', tarGzPath, '-C', extractDir, '--strip-components=1'], {
|
const tar = spawnSync(
|
||||||
stdio: 'inherit'
|
'tar',
|
||||||
});
|
['-xzf', tarGzPath, '-C', extractDir, '--strip-components=1'],
|
||||||
|
{
|
||||||
|
stdio: 'inherit'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (tar.status === 0) {
|
if (tar.status === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(`tar extraction failed with status ${tar.status}`));
|
reject(
|
||||||
|
new Error(`tar extraction failed with status ${tar.status}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
if (process.platform !== 'linux') {
|
||||||
|
console.log('Skipping .NET runtime download on non-Linux platform');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`Downloading .NET ${DOTNET_VERSION} runtime...`);
|
console.log(`Downloading .NET ${DOTNET_VERSION} runtime...`);
|
||||||
|
|
||||||
if (!fs.existsSync(DOTNET_RUNTIME_DIR)) {
|
if (!fs.existsSync(DOTNET_RUNTIME_DIR)) {
|
||||||
fs.mkdirSync(DOTNET_RUNTIME_DIR, { recursive: true });
|
fs.mkdirSync(DOTNET_RUNTIME_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
if (!fs.existsSync(DOTNET_BIN_DIR)) {
|
|
||||||
fs.mkdirSync(DOTNET_BIN_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const tarGzPath = path.join(DOTNET_RUNTIME_DIR, 'dotnet-runtime.tar.gz');
|
const tarGzPath = path.join(DOTNET_RUNTIME_DIR, 'dotnet-runtime.tar.gz');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Download .NET runtime
|
// Download .NET runtime
|
||||||
await downloadFile(DOTNET_RUNTIME_URL, tarGzPath);
|
await downloadFile(DOTNET_RUNTIME_URL, tarGzPath);
|
||||||
console.log('Download completed');
|
console.log('Download completed');
|
||||||
|
|
||||||
// Extract .NET runtime to a temporary directory first
|
// Extract .NET runtime to a temporary directory first
|
||||||
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
|
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
|
||||||
if (!fs.existsSync(tempExtractDir)) {
|
if (!fs.existsSync(tempExtractDir)) {
|
||||||
fs.mkdirSync(tempExtractDir, { recursive: true });
|
fs.mkdirSync(tempExtractDir, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Extracting .NET runtime...');
|
console.log('Extracting .NET runtime...');
|
||||||
await extractTarGz(tarGzPath, tempExtractDir);
|
await extractTarGz(tarGzPath, tempExtractDir);
|
||||||
console.log('Extraction completed');
|
console.log('Extraction completed');
|
||||||
|
|
||||||
// Clean up tar.gz file
|
// Clean up tar.gz file
|
||||||
fs.unlinkSync(tarGzPath);
|
fs.unlinkSync(tarGzPath);
|
||||||
console.log('Cleanup completed');
|
console.log('Cleanup completed');
|
||||||
|
|
||||||
// Move dotnet executable to bin directory
|
// Ensure the dotnet executable is executable
|
||||||
const extractedDotnet = path.join(tempExtractDir, 'dotnet');
|
const extractedDotnet = path.join(tempExtractDir, 'dotnet');
|
||||||
const targetDotnet = path.join(DOTNET_BIN_DIR, 'dotnet');
|
fs.chmodSync(extractedDotnet, 0o755);
|
||||||
|
|
||||||
if (fs.existsSync(extractedDotnet)) {
|
|
||||||
fs.renameSync(extractedDotnet, targetDotnet);
|
|
||||||
fs.chmodSync(targetDotnet, 0o755);
|
|
||||||
console.log('Moved dotnet executable to bin directory');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move all other files to the root of dotnet-runtime
|
// Move all other files to the root of dotnet-runtime
|
||||||
const files = fs.readdirSync(tempExtractDir);
|
const files = fs.readdirSync(tempExtractDir);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const sourcePath = path.join(tempExtractDir, file);
|
const sourcePath = path.join(tempExtractDir, file);
|
||||||
const targetPath = path.join(DOTNET_RUNTIME_DIR, file);
|
const targetPath = path.join(DOTNET_RUNTIME_DIR, file);
|
||||||
|
|
||||||
if (fs.existsSync(targetPath)) {
|
if (fs.existsSync(targetPath)) {
|
||||||
if (fs.lstatSync(sourcePath).isDirectory()) {
|
if (fs.lstatSync(sourcePath).isDirectory()) {
|
||||||
// Remove existing directory and move new one
|
// Remove existing directory and move new one
|
||||||
@@ -96,16 +109,16 @@ async function main() {
|
|||||||
fs.unlinkSync(targetPath);
|
fs.unlinkSync(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.renameSync(sourcePath, targetPath);
|
fs.renameSync(sourcePath, targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up temp directory
|
// Clean up temp directory
|
||||||
fs.rmSync(tempExtractDir, { recursive: true, force: true });
|
fs.rmSync(tempExtractDir, { recursive: true, force: true });
|
||||||
|
|
||||||
console.log(`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`);
|
console.log(
|
||||||
console.log(`dotnet executable available at: ${targetDotnet}`);
|
`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error:', error.message);
|
console.error('Error:', error.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -116,4 +129,4 @@ if (require.main === module) {
|
|||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { downloadFile, extractTarGz };
|
module.exports = { downloadFile, extractTarGz };
|
||||||
|
|||||||
@@ -7,19 +7,35 @@ const {
|
|||||||
Tray,
|
Tray,
|
||||||
Menu,
|
Menu,
|
||||||
dialog,
|
dialog,
|
||||||
Notification
|
Notification,
|
||||||
|
nativeImage
|
||||||
} = require('electron');
|
} = require('electron');
|
||||||
const { spawn, spawnSync } = require('child_process');
|
const { spawn, spawnSync } = require('child_process');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
|
||||||
// Include bundled .NET runtime
|
//app.disableHardwareAcceleration();
|
||||||
const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime');
|
|
||||||
const bundledDotnet = path.join(bundledDotNetPath, 'bin', 'dotnet');
|
|
||||||
|
|
||||||
if (fs.existsSync(bundledDotnet)) {
|
if (process.platform === 'linux') {
|
||||||
process.env.DOTNET_ROOT = bundledDotNetPath;
|
// Include bundled .NET runtime
|
||||||
process.env.PATH = `${path.dirname(bundledDotnet)}:${process.env.PATH}`;
|
const bundledDotNetPath = path.join(
|
||||||
|
process.resourcesPath,
|
||||||
|
'dotnet-runtime'
|
||||||
|
);
|
||||||
|
if (fs.existsSync(bundledDotNetPath)) {
|
||||||
|
process.env.DOTNET_ROOT = bundledDotNetPath;
|
||||||
|
process.env.PATH = `${bundledDotNetPath}:${process.env.PATH}`;
|
||||||
|
}
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
const dotnetPath = path.join('/usr/local/share/dotnet');
|
||||||
|
const dotnetPathArm = path.join('/usr/local/share/dotnet/x64');
|
||||||
|
if (fs.existsSync(dotnetPathArm)) {
|
||||||
|
process.env.DOTNET_ROOT = dotnetPathArm;
|
||||||
|
process.env.PATH = `${dotnetPathArm}:${process.env.PATH}`;
|
||||||
|
} else if (fs.existsSync(dotnetPath)) {
|
||||||
|
process.env.DOTNET_ROOT = dotnetPath;
|
||||||
|
process.env.PATH = `${dotnetPath}:${process.env.PATH}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDotNetInstalled()) {
|
if (!isDotNetInstalled()) {
|
||||||
@@ -30,9 +46,11 @@ if (!isDotNetInstalled()) {
|
|||||||
);
|
);
|
||||||
app.quit();
|
app.quit();
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isOverlayActive = false;
|
||||||
|
let appIsQuitting = false;
|
||||||
|
|
||||||
// Get launch arguments
|
// Get launch arguments
|
||||||
let appImagePath = process.env.APPIMAGE;
|
let appImagePath = process.env.APPIMAGE;
|
||||||
const args = process.argv.slice(1);
|
const args = process.argv.slice(1);
|
||||||
@@ -51,6 +69,24 @@ require(path.join(rootDir, 'build/Electron/VRCX-Electron.cjs'));
|
|||||||
const InteropApi = require('./InteropApi');
|
const InteropApi = require('./InteropApi');
|
||||||
const interopApi = new InteropApi();
|
const interopApi = new InteropApi();
|
||||||
|
|
||||||
|
const WRIST_FRAME_WIDTH = 512;
|
||||||
|
const WRIST_FRAME_HEIGHT = 512;
|
||||||
|
const WRIST_FRAME_SIZE = WRIST_FRAME_WIDTH * WRIST_FRAME_HEIGHT * 4;
|
||||||
|
const WRIST_SHM_PATH = '/dev/shm/vrcx_wrist_overlay';
|
||||||
|
|
||||||
|
function createWristOverlayWindowShm() {
|
||||||
|
fs.writeFileSync(WRIST_SHM_PATH, Buffer.alloc(WRIST_FRAME_SIZE + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const HMD_FRAME_WIDTH = 1024;
|
||||||
|
const HMD_FRAME_HEIGHT = 1024;
|
||||||
|
const HMD_FRAME_SIZE = HMD_FRAME_WIDTH * HMD_FRAME_HEIGHT * 4;
|
||||||
|
const HMD_SHM_PATH = '/dev/shm/vrcx_hmd_overlay';
|
||||||
|
|
||||||
|
function createHmdOverlayWindowShm() {
|
||||||
|
fs.writeFileSync(HMD_SHM_PATH, Buffer.alloc(HMD_FRAME_SIZE + 1));
|
||||||
|
}
|
||||||
|
|
||||||
const version = getVersion();
|
const version = getVersion();
|
||||||
interopApi.getDotNetObject('ProgramElectron').PreInit(version, args);
|
interopApi.getDotNetObject('ProgramElectron').PreInit(version, args);
|
||||||
interopApi.getDotNetObject('VRCXStorage').Load();
|
interopApi.getDotNetObject('VRCXStorage').Load();
|
||||||
@@ -61,6 +97,10 @@ interopApi.getDotNetObject('Discord').Init();
|
|||||||
interopApi.getDotNetObject('WebApi').Init();
|
interopApi.getDotNetObject('WebApi').Init();
|
||||||
interopApi.getDotNetObject('LogWatcher').Init();
|
interopApi.getDotNetObject('LogWatcher').Init();
|
||||||
|
|
||||||
|
interopApi.getDotNetObject('IPCServer').Init();
|
||||||
|
interopApi.getDotNetObject('SystemMonitorElectron').Init();
|
||||||
|
interopApi.getDotNetObject('AppApiVrElectron').Init();
|
||||||
|
|
||||||
ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => {
|
ipcMain.handle('callDotNetMethod', (event, className, methodName, args) => {
|
||||||
return interopApi.callMethod(className, methodName, args);
|
return interopApi.callMethod(className, methodName, args);
|
||||||
});
|
});
|
||||||
@@ -142,6 +182,45 @@ ipcMain.handle('app:restart', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('app:getWristOverlayWindow', () => {
|
||||||
|
if (wristOverlayWindow && wristOverlayWindow.webContents) {
|
||||||
|
return !wristOverlayWindow.webContents.isLoading() &&
|
||||||
|
wristOverlayWindow.webContents.isPainting();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('app:getHmdOverlayWindow', () => {
|
||||||
|
if (hmdOverlayWindow && hmdOverlayWindow.webContents) {
|
||||||
|
return !hmdOverlayWindow.webContents.isLoading() &&
|
||||||
|
hmdOverlayWindow.webContents.isPainting();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
'app:updateVr',
|
||||||
|
(event, active, hmdOverlay, wristOverlay, menuButton, overlayHand) => {
|
||||||
|
if (!active) {
|
||||||
|
disposeOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isOverlayActive = true;
|
||||||
|
|
||||||
|
if (!hmdOverlay) {
|
||||||
|
destroyHmdOverlayWindow();
|
||||||
|
} else if (active && !hmdOverlayWindow) {
|
||||||
|
createHmdOverlayWindowOffscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wristOverlay) {
|
||||||
|
destroyWristOverlayWindow();
|
||||||
|
} else if (active && !wristOverlayWindow) {
|
||||||
|
createWristOverlayWindowOffscreen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function tryRelaunchWithArgs(args) {
|
function tryRelaunchWithArgs(args) {
|
||||||
if (
|
if (
|
||||||
process.platform !== 'linux' ||
|
process.platform !== 'linux' ||
|
||||||
@@ -188,14 +267,11 @@ function createWindow() {
|
|||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js')
|
preload: path.join(__dirname, 'preload.js')
|
||||||
},
|
|
||||||
webContents: {
|
|
||||||
userAgent: version
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
applyWindowState();
|
applyWindowState();
|
||||||
const indexPath = path.join(rootDir, 'build/html/index.html');
|
const indexPath = path.join(rootDir, 'build/html/index.html');
|
||||||
mainWindow.loadFile(indexPath, { userAgent: version });
|
mainWindow.loadFile(indexPath);
|
||||||
|
|
||||||
// add proxy config, doesn't work, thanks electron
|
// add proxy config, doesn't work, thanks electron
|
||||||
// const proxy = VRCXStorage.Get('VRCX_Proxy');
|
// const proxy = VRCXStorage.Get('VRCX_Proxy');
|
||||||
@@ -234,7 +310,7 @@ function createWindow() {
|
|||||||
|
|
||||||
mainWindow.on('close', (event) => {
|
mainWindow.on('close', (event) => {
|
||||||
isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
|
isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
|
||||||
if (isCloseToTray && !app.isQuitting) {
|
if (isCloseToTray && !appIsQuitting) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mainWindow.hide();
|
mainWindow.hide();
|
||||||
}
|
}
|
||||||
@@ -271,8 +347,142 @@ function createWindow() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let wristOverlayWindow = undefined;
|
||||||
|
|
||||||
|
function createWristOverlayWindowOffscreen() {
|
||||||
|
if (!fs.existsSync(WRIST_SHM_PATH)) {
|
||||||
|
createWristOverlayWindowShm();
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
|
||||||
|
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
|
||||||
|
const width = WRIST_FRAME_WIDTH;
|
||||||
|
const height = WRIST_FRAME_HEIGHT;
|
||||||
|
|
||||||
|
wristOverlayWindow = new BrowserWindow({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
icon: path.join(rootDir, 'VRCX.png'),
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
transparent: true,
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
offscreen: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wristOverlayWindow.webContents.setFrameRate(2);
|
||||||
|
|
||||||
|
const indexPath = path.join(rootDir, 'build/html/vr.html');
|
||||||
|
const fileUrl = `file://${indexPath}?1`;
|
||||||
|
wristOverlayWindow.loadURL(fileUrl, { userAgent: version });
|
||||||
|
|
||||||
|
// Use paint event for offscreen rendering
|
||||||
|
wristOverlayWindow.webContents.on('paint', (event, dirty, image) => {
|
||||||
|
const buffer = image.toBitmap();
|
||||||
|
//console.log('Captured wrist frame via paint event, size:', buffer.length);
|
||||||
|
writeWristFrame(buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeWristFrame(imageBuffer) {
|
||||||
|
try {
|
||||||
|
const fd = fs.openSync(WRIST_SHM_PATH, 'r+');
|
||||||
|
const buffer = Buffer.alloc(WRIST_FRAME_SIZE + 1);
|
||||||
|
buffer[0] = 0; // not ready
|
||||||
|
imageBuffer.copy(buffer, 1, 0, WRIST_FRAME_SIZE);
|
||||||
|
buffer[0] = 1; // ready
|
||||||
|
fs.writeSync(fd, buffer);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
//console.log('Wrote wrist frame to shared memory');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error writing wrist frame to shared memory:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyWristOverlayWindow() {
|
||||||
|
if (wristOverlayWindow && !wristOverlayWindow.isDestroyed()) {
|
||||||
|
wristOverlayWindow.close();
|
||||||
|
}
|
||||||
|
wristOverlayWindow = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hmdOverlayWindow = undefined;
|
||||||
|
|
||||||
|
function createHmdOverlayWindowOffscreen() {
|
||||||
|
if (!fs.existsSync(HMD_SHM_PATH)) {
|
||||||
|
createHmdOverlayWindowShm();
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = parseInt(VRCXStorage.Get('VRCX_LocationX')) || 0;
|
||||||
|
const y = parseInt(VRCXStorage.Get('VRCX_LocationY')) || 0;
|
||||||
|
const width = HMD_FRAME_WIDTH;
|
||||||
|
const height = HMD_FRAME_HEIGHT;
|
||||||
|
|
||||||
|
hmdOverlayWindow = new BrowserWindow({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
icon: path.join(rootDir, 'VRCX.png'),
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
transparent: true,
|
||||||
|
frame: false,
|
||||||
|
show: false,
|
||||||
|
webPreferences: {
|
||||||
|
offscreen: true,
|
||||||
|
preload: path.join(__dirname, 'preload.js')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
hmdOverlayWindow.webContents.setFrameRate(48);
|
||||||
|
|
||||||
|
const indexPath = path.join(rootDir, 'build/html/vr.html');
|
||||||
|
const fileUrl = `file://${indexPath}?2`;
|
||||||
|
hmdOverlayWindow.loadURL(fileUrl, { userAgent: version });
|
||||||
|
|
||||||
|
// Use paint event for offscreen rendering
|
||||||
|
hmdOverlayWindow.webContents.on('paint', (event, dirty, image) => {
|
||||||
|
const buffer = image.toBitmap();
|
||||||
|
//console.log('Captured HMD frame via paint event, size:', buffer.length);
|
||||||
|
writeHmdFrame(buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeHmdFrame(imageBuffer) {
|
||||||
|
try {
|
||||||
|
const fd = fs.openSync(HMD_SHM_PATH, 'r+');
|
||||||
|
const buffer = Buffer.alloc(HMD_FRAME_SIZE + 1);
|
||||||
|
buffer[0] = 0; // not ready
|
||||||
|
imageBuffer.copy(buffer, 1, 0, HMD_FRAME_SIZE);
|
||||||
|
buffer[0] = 1; // ready
|
||||||
|
fs.writeSync(fd, buffer);
|
||||||
|
fs.closeSync(fd);
|
||||||
|
//console.log('Wrote HMD frame to shared memory');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error writing HMD frame to shared memory:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyHmdOverlayWindow() {
|
||||||
|
if (hmdOverlayWindow && !hmdOverlayWindow.isDestroyed()) {
|
||||||
|
hmdOverlayWindow.close();
|
||||||
|
}
|
||||||
|
hmdOverlayWindow = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function createTray() {
|
function createTray() {
|
||||||
const tray = new Tray(path.join(rootDir, 'images/tray.png'));
|
let tray = null;
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const image = nativeImage.createFromPath(
|
||||||
|
path.join(rootDir, 'images/tray.png')
|
||||||
|
);
|
||||||
|
tray = new Tray(image.resize({ width: 16, height: 16 }));
|
||||||
|
} else {
|
||||||
|
tray = new Tray(path.join(rootDir, 'images/tray.png'));
|
||||||
|
}
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
{
|
{
|
||||||
label: 'Open',
|
label: 'Open',
|
||||||
@@ -296,7 +506,7 @@ function createTray() {
|
|||||||
label: 'Quit VRCX',
|
label: 'Quit VRCX',
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
click: function () {
|
click: function () {
|
||||||
app.isQuitting = true;
|
appIsQuitting = true;
|
||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -507,18 +717,19 @@ function getHomePath() {
|
|||||||
const absoluteHomePath = fs.realpathSync(relativeHomePath);
|
const absoluteHomePath = fs.realpathSync(relativeHomePath);
|
||||||
return absoluteHomePath;
|
return absoluteHomePath;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.error('Error resolving absolute home path:', err);
|
||||||
return relativeHomePath;
|
return relativeHomePath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersion() {
|
function getVersion() {
|
||||||
try {
|
try {
|
||||||
var versionFile = fs
|
const versionFile = fs
|
||||||
.readFileSync(path.join(rootDir, 'Version'), 'utf8')
|
.readFileSync(path.join(rootDir, 'Version'), 'utf8')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// look for trailing git hash "-22bcd96" to indicate nightly build
|
// look for trailing git hash "-22bcd96" to indicate nightly build
|
||||||
var version = versionFile.split('-');
|
const version = versionFile.split('-');
|
||||||
console.log('Version:', versionFile);
|
console.log('Version:', versionFile);
|
||||||
if (version.length > 0 && version[version.length - 1].length == 7) {
|
if (version.length > 0 && version[version.length - 1].length == 7) {
|
||||||
return `VRCX (Linux) Nightly ${versionFile}`;
|
return `VRCX (Linux) Nightly ${versionFile}`;
|
||||||
@@ -532,19 +743,15 @@ function getVersion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isDotNetInstalled() {
|
function isDotNetInstalled() {
|
||||||
if (process.platform === 'darwin') {
|
let dotnetPath = path.join(process.env.DOTNET_ROOT, 'dotnet');
|
||||||
// Assume .NET is already installed on macOS
|
if (!process.env.DOTNET_ROOT || !fs.existsSync(dotnetPath)) {
|
||||||
return true;
|
// fallback to command
|
||||||
|
dotnetPath = 'dotnet';
|
||||||
}
|
}
|
||||||
|
console.log('Checking for .NET installation at:', dotnetPath);
|
||||||
// Check for bundled .NET runtime
|
|
||||||
if (fs.existsSync(bundledDotnet)) {
|
|
||||||
console.log('Using bundled .NET runtime at:', bundledDotNetPath);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to system .NET runtime
|
// Fallback to system .NET runtime
|
||||||
const result = spawnSync('dotnet', ['--list-runtimes'], {
|
const result = spawnSync(dotnetPath, ['--list-runtimes'], {
|
||||||
encoding: 'utf-8'
|
encoding: 'utf-8'
|
||||||
});
|
});
|
||||||
return result.stdout?.includes('.NETCore.App 9.0');
|
return result.stdout?.includes('.NETCore.App 9.0');
|
||||||
@@ -611,20 +818,54 @@ function applyWindowState() {
|
|||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
createTray();
|
createTray();
|
||||||
|
|
||||||
|
if (process.platform === 'linux') {
|
||||||
|
createWristOverlayWindowOffscreen();
|
||||||
|
createHmdOverlayWindowOffscreen();
|
||||||
|
}
|
||||||
|
|
||||||
installVRCX();
|
installVRCX();
|
||||||
|
|
||||||
app.on('activate', function () {
|
app.on('activate', function () {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
|
createWindow();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// app.on('before-quit', function () {
|
function disposeOverlay() {
|
||||||
// mainWindow.webContents.send('windowClosed');
|
if (!isOverlayActive) {
|
||||||
// });
|
return;
|
||||||
|
}
|
||||||
|
isOverlayActive = false;
|
||||||
|
if (wristOverlayWindow) {
|
||||||
|
wristOverlayWindow.close();
|
||||||
|
wristOverlayWindow = undefined;
|
||||||
|
}
|
||||||
|
if (hmdOverlayWindow) {
|
||||||
|
hmdOverlayWindow.close();
|
||||||
|
hmdOverlayWindow = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(WRIST_SHM_PATH)) {
|
||||||
|
fs.unlinkSync(WRIST_SHM_PATH);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(HMD_SHM_PATH)) {
|
||||||
|
fs.unlinkSync(HMD_SHM_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.on('before-quit', function () {
|
||||||
|
disposeOverlay();
|
||||||
|
|
||||||
|
mainWindow.webContents.send('windowClosed');
|
||||||
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', function () {
|
app.on('window-all-closed', function () {
|
||||||
if (process.platform !== 'darwin') app.quit();
|
disposeOverlay();
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
5
src-electron/offscreen-preload.js
Normal file
5
src-electron/offscreen-preload.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron');
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
onUpdateImage: (callback) => ipcRenderer.on('update-image', (event, base64) => callback(base64))
|
||||||
|
});
|
||||||
29
src-electron/offscreen.html
Normal file
29
src-electron/offscreen.html
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Offscreen Mirror</title>
|
||||||
|
<style>
|
||||||
|
body, html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
#mirror {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<img id="mirror" src="" alt="Main Window Mirror">
|
||||||
|
<script>
|
||||||
|
window.electronAPI.onUpdateImage((base64) => {
|
||||||
|
document.getElementById('mirror').src = `data:image/png;base64,${base64}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -31,11 +31,17 @@ managedHostPath = managedHostPath.indexOf('app.asar.unpacked') < 0 ?
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Paths to patch
|
// Paths to patch
|
||||||
let platformName = 'linux';
|
let platformName = '';
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'win32':
|
case 'win32':
|
||||||
platformName = 'win';
|
platformName = 'win';
|
||||||
break;
|
break;
|
||||||
|
case 'darwin':
|
||||||
|
platformName = 'mac';
|
||||||
|
break;
|
||||||
|
case 'linux':
|
||||||
|
platformName = 'linux';
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
const postBuildPath = path.join(
|
const postBuildPath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
|
|||||||
@@ -30,5 +30,9 @@ contextBridge.exposeInMainWorld('electron', {
|
|||||||
ipcRenderer.on('setWindowState', callback),
|
ipcRenderer.on('setWindowState', callback),
|
||||||
desktopNotification: (title, body, icon) =>
|
desktopNotification: (title, body, icon) =>
|
||||||
ipcRenderer.invoke('notification:showNotification', title, body, icon),
|
ipcRenderer.invoke('notification:showNotification', title, body, icon),
|
||||||
restartApp: () => ipcRenderer.invoke('app:restart')
|
restartApp: () => ipcRenderer.invoke('app:restart'),
|
||||||
});
|
getWristOverlayWindow: () => ipcRenderer.invoke('app:getWristOverlayWindow'),
|
||||||
|
getHmdOverlayWindow: () => ipcRenderer.invoke('app:getHmdOverlayWindow'),
|
||||||
|
updateVr: (active, hmdOverlay, wristOverlay, menuButton, overlayHand) =>
|
||||||
|
ipcRenderer.invoke('app:updateVr', active, hmdOverlay, wristOverlay, menuButton, overlayHand)
|
||||||
|
});
|
||||||
@@ -17,17 +17,34 @@ try {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
|
if (process.platform === 'linux') {
|
||||||
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
|
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
|
||||||
|
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(oldAppImage)) {
|
if (fs.existsSync(oldAppImage)) {
|
||||||
fs.renameSync(oldAppImage, newAppImage);
|
fs.renameSync(oldAppImage, newAppImage);
|
||||||
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
|
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
|
||||||
} else {
|
} else {
|
||||||
console.log(`File not found: ${oldAppImage}`);
|
console.log(`File not found: ${oldAppImage}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error renaming files:', err);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} else if (process.platform === 'darwin') {
|
||||||
console.error('Error renaming files:', err);
|
const oldDmg = path.join(buildDir, `VRCX_Version.dmg`);
|
||||||
process.exit(1);
|
const newDmg = path.join(buildDir, `VRCX_${version}.dmg`);
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(oldDmg)) {
|
||||||
|
fs.renameSync(oldDmg, newDmg);
|
||||||
|
console.log(`Renamed: ${oldDmg} -> ${newDmg}`);
|
||||||
|
} else {
|
||||||
|
console.log(`File not found: ${oldDmg}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error renaming files:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('No renaming needed for this platform.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,5 @@ if (WINDOWS) {
|
|||||||
window.LogWatcher = InteropApi.LogWatcher;
|
window.LogWatcher = InteropApi.LogWatcher;
|
||||||
window.Discord = InteropApi.Discord;
|
window.Discord = InteropApi.Discord;
|
||||||
window.AssetBundleManager = InteropApi.AssetBundleManager;
|
window.AssetBundleManager = InteropApi.AssetBundleManager;
|
||||||
}
|
window.AppApiVrElectron = InteropApi.AppApiVrElectron;
|
||||||
|
}
|
||||||
@@ -151,35 +151,35 @@ function updateTrustColorClasses(trustColor) {
|
|||||||
document.getElementsByTagName('head')[0].appendChild(style);
|
document.getElementsByTagName('head')[0].appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCustomCss() {
|
async function refreshCustomCss() {
|
||||||
if (document.contains(document.getElementById('app-custom-style'))) {
|
if (document.contains(document.getElementById('app-custom-style'))) {
|
||||||
document.getElementById('app-custom-style').remove();
|
document.getElementById('app-custom-style').remove();
|
||||||
}
|
}
|
||||||
AppApi.CustomCssPath().then((customCss) => {
|
const customCss = await AppApi.CustomCss();
|
||||||
|
if (customCss) {
|
||||||
const head = document.head;
|
const head = document.head;
|
||||||
if (customCss) {
|
const $appCustomStyle = document.createElement('link');
|
||||||
const $appCustomStyle = document.createElement('link');
|
$appCustomStyle.setAttribute('id', 'app-custom-style');
|
||||||
$appCustomStyle.setAttribute('id', 'app-custom-style');
|
$appCustomStyle.rel = 'stylesheet';
|
||||||
$appCustomStyle.rel = 'stylesheet';
|
$appCustomStyle.type = 'text/css';
|
||||||
$appCustomStyle.href = `file://${customCss}?_=${Date.now()}`;
|
$appCustomStyle.textContent = customCss;
|
||||||
head.appendChild($appCustomStyle);
|
head.appendChild($appCustomStyle);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshCustomScript() {
|
async function refreshCustomScript() {
|
||||||
if (document.contains(document.getElementById('app-custom-script'))) {
|
if (document.contains(document.getElementById('app-custom-script'))) {
|
||||||
document.getElementById('app-custom-script').remove();
|
document.getElementById('app-custom-script').remove();
|
||||||
}
|
}
|
||||||
AppApi.CustomScriptPath().then((customScript) => {
|
const customScript = await AppApi.CustomScript();
|
||||||
|
if (customScript) {
|
||||||
const head = document.head;
|
const head = document.head;
|
||||||
if (customScript) {
|
const $appCustomScript = document.createElement('script');
|
||||||
const $appCustomScript = document.createElement('script');
|
$appCustomScript.setAttribute('id', 'app-custom-script');
|
||||||
$appCustomScript.setAttribute('id', 'app-custom-script');
|
$appCustomScript.type = 'text/javascript';
|
||||||
$appCustomScript.src = `file://${customScript}?_=${Date.now()}`;
|
$appCustomScript.textContent = customScript;
|
||||||
head.appendChild($appCustomScript);
|
head.appendChild($appCustomScript);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ function parseLocation(tag) {
|
|||||||
ctx.isPrivate = true;
|
ctx.isPrivate = true;
|
||||||
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
|
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
|
||||||
ctx.isTraveling = true;
|
ctx.isTraveling = true;
|
||||||
} else if (!_tag.startsWith('local')) {
|
} else if (tag && !_tag.startsWith('local')) {
|
||||||
ctx.isRealInstance = true;
|
ctx.isRealInstance = true;
|
||||||
const sep = _tag.indexOf(':');
|
const sep = _tag.indexOf(':');
|
||||||
// technically not part of instance id, but might be there when coping id from url so why not support it
|
// technically not part of instance id, but might be there when coping id from url so why not support it
|
||||||
|
|||||||
@@ -385,31 +385,35 @@ export const useAvatarStore = defineStore('Avatar', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* aka: `$app.methods.addAvatarToHistory`
|
|
||||||
* @param {string} avatarId
|
* @param {string} avatarId
|
||||||
*/
|
*/
|
||||||
function addAvatarToHistory(avatarId) {
|
function addAvatarToHistory(avatarId) {
|
||||||
avatarRequest.getAvatar({ avatarId }).then((args) => {
|
avatarRequest
|
||||||
const ref = applyAvatar(args.json);
|
.getAvatar({ avatarId })
|
||||||
|
.then((args) => {
|
||||||
|
const ref = applyAvatar(args.json);
|
||||||
|
|
||||||
database.addAvatarToCache(ref);
|
database.addAvatarToCache(ref);
|
||||||
database.addAvatarToHistory(ref.id);
|
database.addAvatarToHistory(ref.id);
|
||||||
|
|
||||||
if (ref.authorId === userStore.currentUser.id) {
|
if (ref.authorId === userStore.currentUser.id) {
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
const historyArray = state.avatarHistoryArray;
|
|
||||||
for (let i = 0; i < historyArray.length; ++i) {
|
|
||||||
if (historyArray[i].id === ref.id) {
|
|
||||||
historyArray.splice(i, 1);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
state.avatarHistoryArray.unshift(ref);
|
const historyArray = state.avatarHistoryArray;
|
||||||
state.avatarHistory.delete(ref.id);
|
for (let i = 0; i < historyArray.length; ++i) {
|
||||||
state.avatarHistory.add(ref.id);
|
if (historyArray[i].id === ref.id) {
|
||||||
});
|
historyArray.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.avatarHistoryArray.unshift(ref);
|
||||||
|
state.avatarHistory.delete(ref.id);
|
||||||
|
state.avatarHistory.add(ref.id);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to add avatar to history:', err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAvatarHistory() {
|
function clearAvatarHistory() {
|
||||||
|
|||||||
@@ -531,10 +531,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
|
|||||||
|
|
||||||
state.folderSelectorDialogVisible = true;
|
state.folderSelectorDialogVisible = true;
|
||||||
let newFolder = '';
|
let newFolder = '';
|
||||||
if (LINUX) {
|
if (WINDOWS) {
|
||||||
newFolder = await window.electron.openDirectoryDialog();
|
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
|
||||||
} else {
|
} else {
|
||||||
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
|
newFolder = await window.electron.openDirectoryDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.folderSelectorDialogVisible = false;
|
state.folderSelectorDialogVisible = false;
|
||||||
|
|||||||
@@ -362,10 +362,10 @@ export const useNotificationsSettingsStore = defineStore(
|
|||||||
|
|
||||||
function getTTSVoiceName() {
|
function getTTSVoiceName() {
|
||||||
let voices;
|
let voices;
|
||||||
if (LINUX) {
|
if (WINDOWS) {
|
||||||
voices = state.TTSvoices;
|
|
||||||
} else {
|
|
||||||
voices = speechSynthesis.getVoices();
|
voices = speechSynthesis.getVoices();
|
||||||
|
} else {
|
||||||
|
voices = state.TTSvoices;
|
||||||
}
|
}
|
||||||
if (voices.length === 0) {
|
if (voices.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
@@ -379,10 +379,10 @@ export const useNotificationsSettingsStore = defineStore(
|
|||||||
async function changeTTSVoice(index) {
|
async function changeTTSVoice(index) {
|
||||||
setNotificationTTSVoice(index);
|
setNotificationTTSVoice(index);
|
||||||
let voices;
|
let voices;
|
||||||
if (LINUX) {
|
if (WINDOWS) {
|
||||||
voices = state.TTSvoices;
|
|
||||||
} else {
|
|
||||||
voices = speechSynthesis.getVoices();
|
voices = speechSynthesis.getVoices();
|
||||||
|
} else {
|
||||||
|
voices = state.TTSvoices;
|
||||||
}
|
}
|
||||||
if (voices.length === 0) {
|
if (voices.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -133,16 +133,16 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
|
|||||||
(vrcxStore.isRunningUnderWine || LINUX) &&
|
(vrcxStore.isRunningUnderWine || LINUX) &&
|
||||||
--state.nextGameRunningCheck <= 0
|
--state.nextGameRunningCheck <= 0
|
||||||
) {
|
) {
|
||||||
if (LINUX) {
|
if (WINDOWS) {
|
||||||
|
state.nextGameRunningCheck = 3;
|
||||||
|
AppApi.CheckGameRunning();
|
||||||
|
} else {
|
||||||
state.nextGameRunningCheck = 1;
|
state.nextGameRunningCheck = 1;
|
||||||
gameStore.updateIsGameRunning(
|
gameStore.updateIsGameRunning(
|
||||||
await AppApi.IsGameRunning(),
|
await AppApi.IsGameRunning(),
|
||||||
await AppApi.IsSteamVRRunning(),
|
await AppApi.IsSteamVRRunning(),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
state.nextGameRunningCheck = 3;
|
|
||||||
AppApi.CheckGameRunning();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (--state.nextDatabaseOptimize <= 0) {
|
if (--state.nextDatabaseOptimize <= 0) {
|
||||||
|
|||||||
@@ -851,12 +851,7 @@ export const useUserStore = defineStore('User', () => {
|
|||||||
}
|
}
|
||||||
applyUserDialogLocation(true);
|
applyUserDialogLocation(true);
|
||||||
|
|
||||||
if (
|
userRequest.getUser(args.params);
|
||||||
args.cache &&
|
|
||||||
args.ref.$lastFetch < Date.now() - 10000 // 10 seconds
|
|
||||||
) {
|
|
||||||
userRequest.getUser(args.params);
|
|
||||||
}
|
|
||||||
let inCurrentWorld = false;
|
let inCurrentWorld = false;
|
||||||
if (
|
if (
|
||||||
locationStore.lastLocation.playerList.has(D.ref.id)
|
locationStore.lastLocation.playerList.has(D.ref.id)
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ export const useVrStore = defineStore('Vr', () => {
|
|||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const sharedFeedStore = useSharedFeedStore();
|
const sharedFeedStore = useSharedFeedStore();
|
||||||
|
|
||||||
const state = reactive({});
|
const state = reactive({
|
||||||
|
overlayActive: false
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => watchState.isFriendsLoaded,
|
() => watchState.isFriendsLoaded,
|
||||||
@@ -48,6 +50,8 @@ export const useVrStore = defineStore('Vr', () => {
|
|||||||
sharedFeedStore.updateSharedFeed(true);
|
sharedFeedStore.updateSharedFeed(true);
|
||||||
friendStore.onlineFriendCount = 0; // force an update
|
friendStore.onlineFriendCount = 0; // force an update
|
||||||
friendStore.updateOnlineFriendCoutner();
|
friendStore.updateOnlineFriendCoutner();
|
||||||
|
|
||||||
|
state.overlayActive = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveOpenVROption() {
|
async function saveOpenVROption() {
|
||||||
@@ -74,7 +78,7 @@ export const useVrStore = defineStore('Vr', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let onlineFor = '';
|
let onlineFor = null;
|
||||||
if (!wristOverlaySettingsStore.hideUptimeFromFeed) {
|
if (!wristOverlaySettingsStore.hideUptimeFromFeed) {
|
||||||
onlineFor = userStore.currentUser.$online_for;
|
onlineFor = userStore.currentUser.$online_for;
|
||||||
}
|
}
|
||||||
@@ -125,6 +129,13 @@ export const useVrStore = defineStore('Vr', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateOpenVR() {
|
function updateOpenVR() {
|
||||||
|
let newState = {
|
||||||
|
active: false,
|
||||||
|
hmdOverlay: false,
|
||||||
|
wristOverlay: false,
|
||||||
|
menuButton: false,
|
||||||
|
overlayHand: 0
|
||||||
|
};
|
||||||
if (
|
if (
|
||||||
notificationsSettingsStore.openVR &&
|
notificationsSettingsStore.openVR &&
|
||||||
gameStore.isSteamVRRunning &&
|
gameStore.isSteamVRRunning &&
|
||||||
@@ -140,16 +151,42 @@ export const useVrStore = defineStore('Vr', () => {
|
|||||||
) {
|
) {
|
||||||
hmdOverlay = true;
|
hmdOverlay = true;
|
||||||
}
|
}
|
||||||
// active, hmdOverlay, wristOverlay, menuButton, overlayHand
|
newState = {
|
||||||
AppApi.SetVR(
|
active: true,
|
||||||
true,
|
|
||||||
hmdOverlay,
|
hmdOverlay,
|
||||||
wristOverlaySettingsStore.overlayWrist,
|
wristOverlay: wristOverlaySettingsStore.overlayWrist,
|
||||||
wristOverlaySettingsStore.overlaybutton,
|
menuButton: wristOverlaySettingsStore.overlaybutton,
|
||||||
wristOverlaySettingsStore.overlayHand
|
overlayHand: wristOverlaySettingsStore.overlayHand
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AppApi.SetVR(
|
||||||
|
newState.active,
|
||||||
|
newState.hmdOverlay,
|
||||||
|
newState.wristOverlay,
|
||||||
|
newState.menuButton,
|
||||||
|
newState.overlayHand
|
||||||
|
);
|
||||||
|
|
||||||
|
if (LINUX) {
|
||||||
|
window.electron.updateVr(
|
||||||
|
newState.active,
|
||||||
|
newState.hmdOverlay,
|
||||||
|
newState.wristOverlay,
|
||||||
|
newState.menuButton,
|
||||||
|
newState.overlayHand
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
AppApi.SetVR(false, false, false, false, 0);
|
if (state.overlayActive !== newState.active) {
|
||||||
|
if (
|
||||||
|
window.electron.getWristOverlayWindow() ||
|
||||||
|
window.electron.getHmdOverlayWindow()
|
||||||
|
) {
|
||||||
|
vrInit();
|
||||||
|
state.overlayActive = newState.active;
|
||||||
|
}
|
||||||
|
setTimeout(() => vrInit(), 1000); // give the overlay time to load
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -652,11 +652,11 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
|||||||
|
|
||||||
async function backupVrcRegistry(name) {
|
async function backupVrcRegistry(name) {
|
||||||
let regJson;
|
let regJson;
|
||||||
if (LINUX) {
|
if (WINDOWS) {
|
||||||
|
regJson = await AppApi.GetVRChatRegistry();
|
||||||
|
} else {
|
||||||
regJson = await AppApi.GetVRChatRegistryJson();
|
regJson = await AppApi.GetVRChatRegistryJson();
|
||||||
regJson = JSON.parse(regJson);
|
regJson = JSON.parse(regJson);
|
||||||
} else {
|
|
||||||
regJson = await AppApi.GetVRChatRegistry();
|
|
||||||
}
|
}
|
||||||
const newBackup = {
|
const newBackup = {
|
||||||
name,
|
name,
|
||||||
|
|||||||
55
src/types/globals.d.ts
vendored
55
src/types/globals.d.ts
vendored
@@ -7,6 +7,7 @@ declare global {
|
|||||||
interface Window {
|
interface Window {
|
||||||
$app: any;
|
$app: any;
|
||||||
AppApi: AppApi;
|
AppApi: AppApi;
|
||||||
|
AppApiVr: AppApiVr;
|
||||||
WebApi: WebApi;
|
WebApi: WebApi;
|
||||||
VRCXStorage: VRCXStorage;
|
VRCXStorage: VRCXStorage;
|
||||||
SQLite: SQLite;
|
SQLite: SQLite;
|
||||||
@@ -51,6 +52,15 @@ declare global {
|
|||||||
Function: (event: any, state: { windowState: any }) => void
|
Function: (event: any, state: { windowState: any }) => void
|
||||||
) => void;
|
) => void;
|
||||||
restartApp: () => Promise<void>;
|
restartApp: () => Promise<void>;
|
||||||
|
getWristOverlayWindow: () => Promise<boolean>;
|
||||||
|
getHmdOverlayWindow: () => Promise<boolean>;
|
||||||
|
updateVr: (
|
||||||
|
active: bool,
|
||||||
|
hmdOverlay: bool,
|
||||||
|
wristOverlay: bool,
|
||||||
|
menuButton: bool,
|
||||||
|
overlayHand: int
|
||||||
|
) => Promise<void>;
|
||||||
};
|
};
|
||||||
__APP_GLOBALS__: {
|
__APP_GLOBALS__: {
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
@@ -141,7 +151,10 @@ declare global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Discord: {
|
const Discord: {
|
||||||
SetTimestamps(startTimestamp: number, endTimestamp: number): void;
|
SetTimestamps(
|
||||||
|
startTimestamp: number,
|
||||||
|
endTimestamp: number
|
||||||
|
): Promise<void>;
|
||||||
SetAssets(
|
SetAssets(
|
||||||
bigIcon: string,
|
bigIcon: string,
|
||||||
bigIconText: string,
|
bigIconText: string,
|
||||||
@@ -154,8 +167,8 @@ declare global {
|
|||||||
buttonUrl: string,
|
buttonUrl: string,
|
||||||
appId: string,
|
appId: string,
|
||||||
activityType: number
|
activityType: number
|
||||||
): void;
|
): Promise<void>;
|
||||||
SetText(details: string, state: string): void;
|
SetText(details: string, state: string): Promise<void>;
|
||||||
SetActive(active: boolean): Promise<boolean>;
|
SetActive(active: boolean): Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -202,8 +215,8 @@ declare global {
|
|||||||
GetLaunchCommand(): Promise<string>;
|
GetLaunchCommand(): Promise<string>;
|
||||||
IPCAnnounceStart(): Promise<void>;
|
IPCAnnounceStart(): Promise<void>;
|
||||||
SendIpc(type: string, data: string): Promise<void>;
|
SendIpc(type: string, data: string): Promise<void>;
|
||||||
CustomCssPath(): Promise<string>;
|
CustomCss(): Promise<string>;
|
||||||
CustomScriptPath(): Promise<string>;
|
CustomScript(): Promise<string>;
|
||||||
CurrentCulture(): Promise<string>;
|
CurrentCulture(): Promise<string>;
|
||||||
CurrentLanguage(): Promise<string>;
|
CurrentLanguage(): Promise<string>;
|
||||||
GetVersion(): Promise<string>;
|
GetVersion(): Promise<string>;
|
||||||
@@ -355,21 +368,23 @@ declare global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppApiVr: {
|
const AppApiVr: {
|
||||||
Init(): void;
|
Init(): Promise<void>;
|
||||||
VrInit(): void;
|
VrInit(): Promise<void>;
|
||||||
ToggleSystemMonitor(enabled: boolean): void;
|
ToggleSystemMonitor(enabled: boolean): Promise<void>;
|
||||||
CpuUsage(): number;
|
CpuUsage(): Promise<number>;
|
||||||
GetVRDevices(): string[][];
|
GetVRDevices(): Promise<string[][]>;
|
||||||
GetUptime(): number;
|
GetUptime(): Promise<number>;
|
||||||
CurrentCulture(): string;
|
CurrentCulture(): Promise<string>;
|
||||||
CustomVrScriptPath(): string;
|
CustomVrScript(): Promise<string>;
|
||||||
IsRunningUnderWine(): boolean;
|
IsRunningUnderWine(): Promise<boolean>;
|
||||||
|
GetExecuteVrFeedFunctionQueue(): Promise<Map<string, string>>;
|
||||||
|
GetExecuteVrOverlayFunctionQueue(): Promise<Map<string, string>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const WebApi: {
|
const WebApi: {
|
||||||
ClearCookies(): void;
|
ClearCookies(): Promise<void>;
|
||||||
GetCookies(): string;
|
GetCookies(): Promise<string>;
|
||||||
SetCookies(cookie: string): void;
|
SetCookies(cookie: string): Promise<void>;
|
||||||
Execute(options: any): Promise<{ Item1: number; Item2: string }>;
|
Execute(options: any): Promise<{ Item1: number; Item2: string }>;
|
||||||
ExecuteJson(requestJson: string): Promise<string>;
|
ExecuteJson(requestJson: string): Promise<string>;
|
||||||
};
|
};
|
||||||
@@ -399,9 +414,9 @@ declare global {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const webApiService: {
|
const webApiService: {
|
||||||
clearCookies(): void;
|
clearCookies(): Promise<void>;
|
||||||
getCookies(): string;
|
getCookies(): Promise<string>;
|
||||||
setCookies(cookie: string): void;
|
setCookies(cookie: string): Promise<void>;
|
||||||
execute(options: {
|
execute(options: {
|
||||||
url: string;
|
url: string;
|
||||||
method: string;
|
method: string;
|
||||||
|
|||||||
2
src/types/user.d.ts
vendored
2
src/types/user.d.ts
vendored
@@ -128,7 +128,7 @@ interface getCurrentUserResponse extends getUserResponse {
|
|||||||
oculusId: string;
|
oculusId: string;
|
||||||
offlineFriends: string[];
|
offlineFriends: string[];
|
||||||
onlineFriends: string[];
|
onlineFriends: string[];
|
||||||
pastDisplayNames: { displayName: string; dateChanged: string }[];
|
pastDisplayNames: { displayName: string; updated_at: string }[];
|
||||||
picoId: string;
|
picoId: string;
|
||||||
presence?: {
|
presence?: {
|
||||||
avatarThumbnail: string;
|
avatarThumbnail: string;
|
||||||
|
|||||||
@@ -595,6 +595,7 @@
|
|||||||
inputPattern: /\S+/,
|
inputPattern: /\S+/,
|
||||||
inputErrorMessage: t('prompt.direct_access_user_id.input_error'),
|
inputErrorMessage: t('prompt.direct_access_user_id.input_error'),
|
||||||
callback: (action, instance) => {
|
callback: (action, instance) => {
|
||||||
|
instance.inputValue = instance.inputValue.trim();
|
||||||
if (action === 'confirm' && instance.inputValue) {
|
if (action === 'confirm' && instance.inputValue) {
|
||||||
const testUrl = instance.inputValue.substring(0, 15);
|
const testUrl = instance.inputValue.substring(0, 15);
|
||||||
if (testUrl === 'https://vrchat.') {
|
if (testUrl === 'https://vrchat.') {
|
||||||
@@ -622,6 +623,7 @@
|
|||||||
inputPattern: /\S+/,
|
inputPattern: /\S+/,
|
||||||
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
|
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
|
||||||
callback: (action, instance) => {
|
callback: (action, instance) => {
|
||||||
|
instance.inputValue = instance.inputValue.trim();
|
||||||
if (action === 'confirm' && instance.inputValue) {
|
if (action === 'confirm' && instance.inputValue) {
|
||||||
if (!directAccessWorld(instance.inputValue)) {
|
if (!directAccessWorld(instance.inputValue)) {
|
||||||
$message({
|
$message({
|
||||||
@@ -641,6 +643,7 @@
|
|||||||
inputPattern: /\S+/,
|
inputPattern: /\S+/,
|
||||||
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error'),
|
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error'),
|
||||||
callback: (action, instance) => {
|
callback: (action, instance) => {
|
||||||
|
instance.inputValue = instance.inputValue.trim();
|
||||||
if (action === 'confirm' && instance.inputValue) {
|
if (action === 'confirm' && instance.inputValue) {
|
||||||
const testUrl = instance.inputValue.substring(0, 15);
|
const testUrl = instance.inputValue.substring(0, 15);
|
||||||
if (testUrl === 'https://vrchat.') {
|
if (testUrl === 'https://vrchat.') {
|
||||||
|
|||||||
@@ -77,32 +77,31 @@
|
|||||||
<!--//- General | Application-->
|
<!--//- General | Application-->
|
||||||
<div class="options-container">
|
<div class="options-container">
|
||||||
<span class="header">{{ t('view.settings.general.application.header') }}</span>
|
<span class="header">{{ t('view.settings.general.application.header') }}</span>
|
||||||
<template v-if="!isLinux">
|
<simple-switch
|
||||||
<simple-switch
|
v-if="!isLinux"
|
||||||
:label="t('view.settings.general.application.startup')"
|
:label="t('view.settings.general.application.startup')"
|
||||||
:value="isStartAtWindowsStartup"
|
:value="isStartAtWindowsStartup"
|
||||||
@change="setIsStartAtWindowsStartup" />
|
@change="setIsStartAtWindowsStartup" />
|
||||||
<simple-switch
|
<simple-switch
|
||||||
:label="t('view.settings.general.application.minimized')"
|
:label="t('view.settings.general.application.minimized')"
|
||||||
:value="isStartAsMinimizedState"
|
:value="isStartAsMinimizedState"
|
||||||
@change="setIsStartAsMinimizedState" />
|
@change="setIsStartAsMinimizedState" />
|
||||||
<simple-switch
|
<simple-switch
|
||||||
:label="t('view.settings.general.application.tray')"
|
:label="t('view.settings.general.application.tray')"
|
||||||
:value="isCloseToTray"
|
:value="isCloseToTray"
|
||||||
@change="setIsCloseToTray" />
|
@change="setIsCloseToTray" />
|
||||||
</template>
|
<simple-switch
|
||||||
<template v-if="!isLinux">
|
v-if="!isLinux"
|
||||||
<simple-switch
|
:label="t('view.settings.general.application.disable_gpu_acceleration')"
|
||||||
:label="t('view.settings.general.application.disable_gpu_acceleration')"
|
:value="disableGpuAcceleration"
|
||||||
:value="disableGpuAcceleration"
|
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
||||||
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
@change="setDisableGpuAcceleration" />
|
||||||
@change="setDisableGpuAcceleration" />
|
<simple-switch
|
||||||
<simple-switch
|
v-if="!isLinux"
|
||||||
:label="t('view.settings.general.application.disable_vr_overlay_gpu_acceleration')"
|
:label="t('view.settings.general.application.disable_vr_overlay_gpu_acceleration')"
|
||||||
:value="disableVrOverlayGpuAcceleration"
|
:value="disableVrOverlayGpuAcceleration"
|
||||||
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
|
||||||
@change="setDisableVrOverlayGpuAcceleration" />
|
@change="setDisableVrOverlayGpuAcceleration" />
|
||||||
</template>
|
|
||||||
<div class="options-container-item">
|
<div class="options-container-item">
|
||||||
<el-button size="small" icon="el-icon-connection" @click="promptProxySettings">{{
|
<el-button size="small" icon="el-icon-connection" @click="promptProxySettings">{{
|
||||||
t('view.settings.general.application.proxy')
|
t('view.settings.general.application.proxy')
|
||||||
@@ -796,7 +795,7 @@
|
|||||||
}}</el-radio-button>
|
}}</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="!isLinux">
|
<template>
|
||||||
<simple-switch
|
<simple-switch
|
||||||
:label="
|
:label="
|
||||||
t('view.settings.notifications.notifications.steamvr_notifications.steamvr_overlay')
|
t('view.settings.notifications.notifications.steamvr_notifications.steamvr_overlay')
|
||||||
@@ -831,6 +830,43 @@
|
|||||||
}}</el-button
|
}}</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="options-container-item">
|
||||||
|
<span class="name" style="vertical-align: top; padding-top: 10px">{{
|
||||||
|
t(
|
||||||
|
'view.settings.notifications.notifications.steamvr_notifications.notification_opacity'
|
||||||
|
)
|
||||||
|
}}</span>
|
||||||
|
<el-slider
|
||||||
|
:value="notificationOpacity"
|
||||||
|
@input="setNotificationOpacity"
|
||||||
|
:show-tooltip="false"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
show-input
|
||||||
|
style="display: inline-block; width: 300px" />
|
||||||
|
</div>
|
||||||
|
<div class="options-container-item">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-time"
|
||||||
|
:disabled="(!overlayNotifications || !openVR) && !xsNotifications"
|
||||||
|
@click="promptNotificationTimeout"
|
||||||
|
>{{
|
||||||
|
t(
|
||||||
|
'view.settings.notifications.notifications.steamvr_notifications.notification_timeout'
|
||||||
|
)
|
||||||
|
}}</el-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<simple-switch
|
||||||
|
:label="t('view.settings.notifications.notifications.steamvr_notifications.user_images')"
|
||||||
|
:value="imageNotifications"
|
||||||
|
@change="
|
||||||
|
setImageNotifications();
|
||||||
|
saveOpenVROption();
|
||||||
|
" />
|
||||||
|
</template>
|
||||||
|
<template v-if="!isLinux">
|
||||||
<simple-switch
|
<simple-switch
|
||||||
:label="
|
:label="
|
||||||
t(
|
t(
|
||||||
@@ -856,61 +892,30 @@
|
|||||||
saveOpenVROption();
|
saveOpenVROption();
|
||||||
" />
|
" />
|
||||||
</template>
|
</template>
|
||||||
<simple-switch
|
<template v-if="!isLinux">
|
||||||
:label="
|
<simple-switch
|
||||||
t(
|
:label="
|
||||||
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:value="ovrtHudNotifications"
|
|
||||||
@change="
|
|
||||||
setOvrtHudNotifications();
|
|
||||||
saveOpenVROption();
|
|
||||||
" />
|
|
||||||
<simple-switch
|
|
||||||
:label="
|
|
||||||
t(
|
|
||||||
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
:value="ovrtWristNotifications"
|
|
||||||
@change="
|
|
||||||
setOvrtWristNotifications();
|
|
||||||
saveOpenVROption();
|
|
||||||
" />
|
|
||||||
<simple-switch
|
|
||||||
:label="t('view.settings.notifications.notifications.steamvr_notifications.user_images')"
|
|
||||||
:value="imageNotifications"
|
|
||||||
@change="
|
|
||||||
setImageNotifications();
|
|
||||||
saveOpenVROption();
|
|
||||||
" />
|
|
||||||
<div class="options-container-item">
|
|
||||||
<span class="name" style="vertical-align: top; padding-top: 10px">{{
|
|
||||||
t('view.settings.notifications.notifications.steamvr_notifications.notification_opacity')
|
|
||||||
}}</span>
|
|
||||||
<el-slider
|
|
||||||
:value="notificationOpacity"
|
|
||||||
@input="setNotificationOpacity"
|
|
||||||
:show-tooltip="false"
|
|
||||||
:min="0"
|
|
||||||
:max="100"
|
|
||||||
show-input
|
|
||||||
style="display: inline-block; width: 300px" />
|
|
||||||
</div>
|
|
||||||
<div class="options-container-item">
|
|
||||||
<el-button
|
|
||||||
size="small"
|
|
||||||
icon="el-icon-time"
|
|
||||||
:disabled="(!overlayNotifications || !openVR) && !xsNotifications"
|
|
||||||
@click="promptNotificationTimeout"
|
|
||||||
>{{
|
|
||||||
t(
|
t(
|
||||||
'view.settings.notifications.notifications.steamvr_notifications.notification_timeout'
|
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
|
||||||
)
|
)
|
||||||
}}</el-button
|
"
|
||||||
>
|
:value="ovrtHudNotifications"
|
||||||
</div>
|
@change="
|
||||||
|
setOvrtHudNotifications();
|
||||||
|
saveOpenVROption();
|
||||||
|
" />
|
||||||
|
<simple-switch
|
||||||
|
:label="
|
||||||
|
t(
|
||||||
|
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:value="ovrtWristNotifications"
|
||||||
|
@change="
|
||||||
|
setOvrtWristNotifications();
|
||||||
|
saveOpenVROption();
|
||||||
|
" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!--//- Notifications | Notifications | Desktop Notifications-->
|
<!--//- Notifications | Notifications | Desktop Notifications-->
|
||||||
<div class="options-container">
|
<div class="options-container">
|
||||||
@@ -1047,7 +1052,7 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!--//- Wrist Overlay Tab-->
|
<!--//- Wrist Overlay Tab-->
|
||||||
<el-tab-pane v-if="!isLinux" lazy :label="t('view.settings.category.wrist_overlay')">
|
<el-tab-pane lazy :label="t('view.settings.category.wrist_overlay')">
|
||||||
<!--//- Wrist Overlay | SteamVR Wrist Overlay-->
|
<!--//- Wrist Overlay | SteamVR Wrist Overlay-->
|
||||||
<div class="options-container" style="margin-top: 0">
|
<div class="options-container" style="margin-top: 0">
|
||||||
<span class="header">{{ t('view.settings.wrist_overlay.steamvr_wrist_overlay.header') }}</span>
|
<span class="header">{{ t('view.settings.wrist_overlay.steamvr_wrist_overlay.header') }}</span>
|
||||||
@@ -1436,19 +1441,15 @@
|
|||||||
saveOpenVROption();
|
saveOpenVROption();
|
||||||
"></simple-switch>
|
"></simple-switch>
|
||||||
<!--//- Advanced | VRChat Quit Fix-->
|
<!--//- Advanced | VRChat Quit Fix-->
|
||||||
<template v-if="!isLinux">
|
<span class="sub-header">{{ t('view.settings.advanced.advanced.vrchat_quit_fix.header') }}</span>
|
||||||
<span class="sub-header">{{
|
<simple-switch
|
||||||
t('view.settings.advanced.advanced.vrchat_quit_fix.header')
|
:label="t('view.settings.advanced.advanced.vrchat_quit_fix.description')"
|
||||||
}}</span>
|
:value="vrcQuitFix"
|
||||||
<simple-switch
|
:long-label="true"
|
||||||
:label="t('view.settings.advanced.advanced.vrchat_quit_fix.description')"
|
@change="
|
||||||
:value="vrcQuitFix"
|
setVrcQuitFix();
|
||||||
:long-label="true"
|
saveOpenVROption();
|
||||||
@change="
|
" />
|
||||||
setVrcQuitFix();
|
|
||||||
saveOpenVROption();
|
|
||||||
" />
|
|
||||||
</template>
|
|
||||||
<!--//- Advanced | Auto Cache Management-->
|
<!--//- Advanced | Auto Cache Management-->
|
||||||
<span class="sub-header">{{
|
<span class="sub-header">{{
|
||||||
t('view.settings.advanced.advanced.auto_cache_management.header')
|
t('view.settings.advanced.advanced.auto_cache_management.header')
|
||||||
@@ -1528,7 +1529,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!--//- Advanced | Video Progress Pie-->
|
<!--//- Advanced | Video Progress Pie-->
|
||||||
<div v-if="!isLinux" class="options-container">
|
<div class="options-container">
|
||||||
<span class="header">{{ t('view.settings.advanced.advanced.video_progress_pie.header') }}</span>
|
<span class="header">{{ t('view.settings.advanced.advanced.video_progress_pie.header') }}</span>
|
||||||
<simple-switch
|
<simple-switch
|
||||||
:label="t('view.settings.advanced.advanced.video_progress_pie.enable')"
|
:label="t('view.settings.advanced.advanced.video_progress_pie.enable')"
|
||||||
|
|||||||
293
src/vr.js
293
src/vr.js
@@ -23,13 +23,19 @@ import { removeFromArray } from './shared/utils/base/array';
|
|||||||
import { timeToText } from './shared/utils/base/format';
|
import { timeToText } from './shared/utils/base/format';
|
||||||
|
|
||||||
import pugTemplate from './vr.pug';
|
import pugTemplate from './vr.pug';
|
||||||
|
import InteropApi from './ipc-electron/interopApi.js';
|
||||||
|
|
||||||
Vue.component('marquee-text', MarqueeText);
|
Vue.component('marquee-text', MarqueeText);
|
||||||
|
|
||||||
(async function () {
|
(async function () {
|
||||||
let $app = {};
|
let $app = {};
|
||||||
|
|
||||||
await CefSharp.BindObjectAsync('AppApiVr');
|
if (WINDOWS) {
|
||||||
|
await CefSharp.BindObjectAsync('AppApiVr');
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
window.AppApiVr = InteropApi.AppApiVrElectron;
|
||||||
|
}
|
||||||
|
|
||||||
Noty.overrideDefaults({
|
Noty.overrideDefaults({
|
||||||
animation: {
|
animation: {
|
||||||
@@ -82,7 +88,7 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
methods: {
|
methods: {
|
||||||
parse() {
|
parse() {
|
||||||
this.text = this.location;
|
this.text = this.location;
|
||||||
var L = parseLocation(this.location);
|
const L = parseLocation(this.location);
|
||||||
if (L.isOffline) {
|
if (L.isOffline) {
|
||||||
this.text = 'Offline';
|
this.text = 'Offline';
|
||||||
} else if (L.isPrivate) {
|
} else if (L.isPrivate) {
|
||||||
@@ -176,9 +182,12 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
watch: {},
|
watch: {},
|
||||||
el: '#root',
|
el: '#root',
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.isRunningUnderWine = await AppApiVr.IsRunningUnderWine();
|
if (WINDOWS) {
|
||||||
await this.applyWineEmojis();
|
this.isRunningUnderWine = await AppApiVr.IsRunningUnderWine();
|
||||||
workerTimers.setTimeout(() => AppApiVr.VrInit(), 5000);
|
await this.applyWineEmojis();
|
||||||
|
} else {
|
||||||
|
this.updateVrElectronLoop();
|
||||||
|
}
|
||||||
if (this.appType === '1') {
|
if (this.appType === '1') {
|
||||||
this.refreshCustomScript();
|
this.refreshCustomScript();
|
||||||
this.updateStatsLoop();
|
this.updateStatsLoop();
|
||||||
@@ -189,97 +198,97 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
Object.assign($app, app);
|
Object.assign($app, app);
|
||||||
|
|
||||||
$app.methods.configUpdate = function (json) {
|
$app.methods.configUpdate = function (json) {
|
||||||
this.config = JSON.parse(json);
|
$app.config = JSON.parse(json);
|
||||||
this.hudFeed = [];
|
$app.hudFeed = [];
|
||||||
this.hudTimeout = [];
|
$app.hudTimeout = [];
|
||||||
this.setDatetimeFormat();
|
$app.setDatetimeFormat();
|
||||||
this.setAppLanguage(this.config.appLanguage);
|
$app.setAppLanguage($app.config.appLanguage);
|
||||||
this.updateFeedLength();
|
$app.updateFeedLength();
|
||||||
if (
|
if (
|
||||||
this.config.vrOverlayCpuUsage !== this.cpuUsageEnabled ||
|
$app.config.vrOverlayCpuUsage !== $app.cpuUsageEnabled ||
|
||||||
this.config.pcUptimeOnFeed !== this.pcUptimeEnabled
|
$app.config.pcUptimeOnFeed !== $app.pcUptimeEnabled
|
||||||
) {
|
) {
|
||||||
this.cpuUsageEnabled = this.config.vrOverlayCpuUsage;
|
$app.cpuUsageEnabled = $app.config.vrOverlayCpuUsage;
|
||||||
this.pcUptimeEnabled = this.config.pcUptimeOnFeed;
|
$app.pcUptimeEnabled = $app.config.pcUptimeOnFeed;
|
||||||
AppApiVr.ToggleSystemMonitor(
|
AppApiVr.ToggleSystemMonitor(
|
||||||
this.cpuUsageEnabled || this.pcUptimeEnabled
|
$app.cpuUsageEnabled || $app.pcUptimeEnabled
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.config.notificationOpacity !== this.notificationOpacity) {
|
if ($app.config.notificationOpacity !== $app.notificationOpacity) {
|
||||||
this.notificationOpacity = this.config.notificationOpacity;
|
$app.notificationOpacity = $app.config.notificationOpacity;
|
||||||
this.setNotyOpacity(this.notificationOpacity);
|
$app.setNotyOpacity($app.notificationOpacity);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.updateOnlineFriendCount = function (count) {
|
$app.methods.updateOnlineFriendCount = function (count) {
|
||||||
this.onlineFriendCount = parseInt(count, 10);
|
$app.onlineFriendCount = parseInt(count, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.nowPlayingUpdate = function (json) {
|
$app.methods.nowPlayingUpdate = function (json) {
|
||||||
this.nowPlaying = JSON.parse(json);
|
$app.nowPlaying = JSON.parse(json);
|
||||||
if (this.appType === '2') {
|
if ($app.appType === '2') {
|
||||||
var circle = document.querySelector('.np-progress-circle-stroke');
|
const circle = document.querySelector('.np-progress-circle-stroke');
|
||||||
if (
|
if (
|
||||||
this.lastLocation.progressPie &&
|
$app.lastLocation.progressPie &&
|
||||||
this.nowPlaying.percentage !== 0
|
$app.nowPlaying.percentage !== 0
|
||||||
) {
|
) {
|
||||||
circle.style.opacity = 0.5;
|
circle.style.opacity = 0.5;
|
||||||
var circumference = circle.getTotalLength();
|
const circumference = circle.getTotalLength();
|
||||||
circle.style.strokeDashoffset =
|
circle.style.strokeDashoffset =
|
||||||
circumference -
|
circumference -
|
||||||
(this.nowPlaying.percentage / 100) * circumference;
|
($app.nowPlaying.percentage / 100) * circumference;
|
||||||
} else {
|
} else {
|
||||||
circle.style.opacity = 0;
|
circle.style.opacity = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.updateFeedLength();
|
$app.updateFeedLength();
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.lastLocationUpdate = function (json) {
|
$app.methods.lastLocationUpdate = function (json) {
|
||||||
this.lastLocation = JSON.parse(json);
|
$app.lastLocation = JSON.parse(json);
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.wristFeedUpdate = function (json) {
|
$app.methods.wristFeedUpdate = function (json) {
|
||||||
this.wristFeed = JSON.parse(json);
|
$app.wristFeed = JSON.parse(json);
|
||||||
this.updateFeedLength();
|
$app.updateFeedLength();
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.updateFeedLength = function () {
|
$app.methods.updateFeedLength = function () {
|
||||||
if (this.appType === '2' || this.wristFeed.length === 0) {
|
if ($app.appType === '2' || $app.wristFeed.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var length = 16;
|
let length = 16;
|
||||||
if (!this.config.hideDevicesFromFeed) {
|
if (!$app.config.hideDevicesFromFeed) {
|
||||||
length -= 2;
|
length -= 2;
|
||||||
if (this.deviceCount > 8) {
|
if ($app.deviceCount > 8) {
|
||||||
length -= 1;
|
length -= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this.nowPlaying.playing) {
|
if ($app.nowPlaying.playing) {
|
||||||
length -= 1;
|
length -= 1;
|
||||||
}
|
}
|
||||||
if (length < this.wristFeed.length) {
|
if (length < $app.wristFeed.length) {
|
||||||
this.wristFeed.length = length;
|
$app.wristFeed.length = length;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.refreshCustomScript = function () {
|
$app.methods.refreshCustomScript = async function () {
|
||||||
if (document.contains(document.getElementById('vr-custom-script'))) {
|
if (document.contains(document.getElementById('vr-custom-script'))) {
|
||||||
document.getElementById('vr-custom-script').remove();
|
document.getElementById('vr-custom-script').remove();
|
||||||
}
|
}
|
||||||
AppApiVr.CustomVrScriptPath().then((customScript) => {
|
const customScript = await AppApiVr.CustomVrScript();
|
||||||
var head = document.head;
|
if (customScript) {
|
||||||
if (customScript) {
|
const head = document.head;
|
||||||
var $vrCustomScript = document.createElement('script');
|
const $vrCustomScript = document.createElement('script');
|
||||||
$vrCustomScript.setAttribute('id', 'vr-custom-script');
|
$vrCustomScript.setAttribute('id', 'vr-custom-script');
|
||||||
$vrCustomScript.src = `file://${customScript}?_=${Date.now()}`;
|
$vrCustomScript.type = 'text/javascript';
|
||||||
head.appendChild($vrCustomScript);
|
$vrCustomScript.textContent = customScript;
|
||||||
}
|
head.appendChild($vrCustomScript);
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.setNotyOpacity = function (value) {
|
$app.methods.setNotyOpacity = function (value) {
|
||||||
var opacity = parseFloat(value / 100).toFixed(2);
|
const opacity = parseFloat(value / 100).toFixed(2);
|
||||||
let element = document.getElementById('noty-opacity');
|
let element = document.getElementById('noty-opacity');
|
||||||
if (!element) {
|
if (!element) {
|
||||||
document.body.insertAdjacentHTML(
|
document.body.insertAdjacentHTML(
|
||||||
@@ -293,43 +302,43 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
|
|
||||||
$app.methods.updateStatsLoop = async function () {
|
$app.methods.updateStatsLoop = async function () {
|
||||||
try {
|
try {
|
||||||
this.currentTime = new Date()
|
$app.currentTime = new Date()
|
||||||
.toLocaleDateString(this.currentCulture, {
|
.toLocaleDateString($app.currentCulture, {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
second: 'numeric',
|
second: 'numeric',
|
||||||
hourCycle: this.config.dtHour12 ? 'h12' : 'h23'
|
hourCycle: $app.config.dtHour12 ? 'h12' : 'h23'
|
||||||
})
|
})
|
||||||
.replace(' AM', ' am')
|
.replace(' AM', ' am')
|
||||||
.replace(' PM', ' pm')
|
.replace(' PM', ' pm')
|
||||||
.replace(',', '');
|
.replace(',', '');
|
||||||
|
|
||||||
if (this.cpuUsageEnabled) {
|
if ($app.cpuUsageEnabled) {
|
||||||
var cpuUsage = await AppApiVr.CpuUsage();
|
const cpuUsage = await AppApiVr.CpuUsage();
|
||||||
this.cpuUsage = cpuUsage.toFixed(0);
|
$app.cpuUsage = cpuUsage.toFixed(0);
|
||||||
}
|
}
|
||||||
if (this.lastLocation.date !== 0) {
|
if ($app.lastLocation.date !== 0) {
|
||||||
this.lastLocationTimer = timeToText(
|
$app.lastLocationTimer = timeToText(
|
||||||
Date.now() - this.lastLocation.date
|
Date.now() - $app.lastLocation.date
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.lastLocationTimer = '';
|
$app.lastLocationTimer = '';
|
||||||
}
|
}
|
||||||
if (this.lastLocation.onlineFor) {
|
if ($app.lastLocation.onlineFor) {
|
||||||
this.onlineForTimer = timeToText(
|
$app.onlineForTimer = timeToText(
|
||||||
Date.now() - this.lastLocation.onlineFor
|
Date.now() - $app.lastLocation.onlineFor
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.onlineForTimer = '';
|
$app.onlineForTimer = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.config.hideDevicesFromFeed) {
|
if (!$app.config.hideDevicesFromFeed) {
|
||||||
AppApiVr.GetVRDevices().then((devices) => {
|
AppApiVr.GetVRDevices().then((devices) => {
|
||||||
var deviceList = [];
|
let deviceList = [];
|
||||||
var baseStations = 0;
|
let baseStations = 0;
|
||||||
devices.forEach((device) => {
|
devices.forEach((device) => {
|
||||||
device[3] = parseInt(device[3], 10);
|
device[3] = parseInt(device[3], 10);
|
||||||
if (device[0] === 'base' && device[1] === 'connected') {
|
if (device[0] === 'base' && device[1] === 'connected') {
|
||||||
@@ -338,7 +347,7 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
deviceList.push(device);
|
deviceList.push(device);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.deviceCount = deviceList.length;
|
$app.deviceCount = deviceList.length;
|
||||||
const deviceValue = (dev) => {
|
const deviceValue = (dev) => {
|
||||||
if (dev[0] === 'headset') return 0;
|
if (dev[0] === 'headset') return 0;
|
||||||
if (dev[0] === 'leftController') return 1;
|
if (dev[0] === 'leftController') return 1;
|
||||||
@@ -368,38 +377,89 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
'',
|
'',
|
||||||
baseStations
|
baseStations
|
||||||
]);
|
]);
|
||||||
this.deviceCount += 1;
|
$app.deviceCount += 1;
|
||||||
}
|
}
|
||||||
this.devices = deviceList;
|
$app.devices = deviceList;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.devices = [];
|
$app.devices = [];
|
||||||
}
|
}
|
||||||
if (this.config.pcUptimeOnFeed) {
|
if ($app.config.pcUptimeOnFeed) {
|
||||||
AppApiVr.GetUptime().then((uptime) => {
|
AppApiVr.GetUptime().then((uptime) => {
|
||||||
if (uptime) {
|
if (uptime) {
|
||||||
this.pcUptime = timeToText(uptime);
|
$app.pcUptime = timeToText(uptime);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.pcUptime = '';
|
$app.pcUptime = '';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
workerTimers.setTimeout(() => this.updateStatsLoop(), 500);
|
workerTimers.setTimeout(() => $app.updateStatsLoop(), 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
$app.methods.updateVrElectronLoop = async function () {
|
||||||
|
try {
|
||||||
|
if ($app.appType === '1') {
|
||||||
|
const wristOverlayQueue =
|
||||||
|
await AppApiVr.GetExecuteVrFeedFunctionQueue();
|
||||||
|
if (wristOverlayQueue) {
|
||||||
|
wristOverlayQueue.forEach((item) => {
|
||||||
|
// item[0] is the function name, item[1] is already an object
|
||||||
|
const fullFunctionName = item[0];
|
||||||
|
const jsonArg = item[1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof window.$app === 'object' &&
|
||||||
|
typeof window.$app[fullFunctionName] === 'function'
|
||||||
|
) {
|
||||||
|
window.$app[fullFunctionName](jsonArg);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`$app.${fullFunctionName} is not defined or is not a function`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const hmdOverlayQueue =
|
||||||
|
await AppApiVr.GetExecuteVrOverlayFunctionQueue();
|
||||||
|
if (hmdOverlayQueue) {
|
||||||
|
hmdOverlayQueue.forEach((item) => {
|
||||||
|
// item[0] is the function name, item[1] is already an object
|
||||||
|
const fullFunctionName = item[0];
|
||||||
|
const jsonArg = item[1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof window.$app === 'object' &&
|
||||||
|
typeof window.$app[fullFunctionName] === 'function'
|
||||||
|
) {
|
||||||
|
window.$app[fullFunctionName](jsonArg);
|
||||||
|
} else {
|
||||||
|
console.error(
|
||||||
|
`$app.${fullFunctionName} is not defined or is not a function`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
workerTimers.setTimeout(() => $app.updateVrElectronLoop(), 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.playNoty = function (json) {
|
$app.methods.playNoty = function (json) {
|
||||||
var { noty, message, image } = JSON.parse(json);
|
let { noty, message, image } = JSON.parse(json);
|
||||||
if (typeof noty === 'undefined') {
|
if (typeof noty === 'undefined') {
|
||||||
console.error('noty is undefined');
|
console.error('noty is undefined');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var noty = escapeTagRecursive(noty);
|
noty = escapeTagRecursive(noty);
|
||||||
var message = escapeTag(message) || '';
|
message = escapeTag(message) || '';
|
||||||
var text = '';
|
let text = '';
|
||||||
var img = '';
|
let img = '';
|
||||||
if (image) {
|
if (image) {
|
||||||
img = `<img class="noty-img" src="${image}"></img>`;
|
img = `<img class="noty-img" src="${image}"></img>`;
|
||||||
}
|
}
|
||||||
@@ -423,7 +483,7 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
)}`;
|
)}`;
|
||||||
break;
|
break;
|
||||||
case 'Online':
|
case 'Online':
|
||||||
var locationName = '';
|
let locationName = '';
|
||||||
if (noty.worldName) {
|
if (noty.worldName) {
|
||||||
locationName = ` to ${displayLocation(
|
locationName = ` to ${displayLocation(
|
||||||
noty.location,
|
noty.location,
|
||||||
@@ -444,7 +504,8 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
noty.senderUsername
|
noty.senderUsername
|
||||||
}</strong> has invited you to ${displayLocation(
|
}</strong> has invited you to ${displayLocation(
|
||||||
noty.details.worldId,
|
noty.details.worldId,
|
||||||
noty.details.worldName
|
noty.details.worldName,
|
||||||
|
''
|
||||||
)}${message}`;
|
)}${message}`;
|
||||||
break;
|
break;
|
||||||
case 'requestInvite':
|
case 'requestInvite':
|
||||||
@@ -556,16 +617,16 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
if (text) {
|
if (text) {
|
||||||
new Noty({
|
new Noty({
|
||||||
type: 'alert',
|
type: 'alert',
|
||||||
theme: this.config.notificationTheme,
|
theme: $app.config.notificationTheme,
|
||||||
timeout: this.config.notificationTimeout,
|
timeout: $app.config.notificationTimeout,
|
||||||
layout: this.config.notificationPosition,
|
layout: $app.config.notificationPosition,
|
||||||
text: `${img}<div class="noty-text">${text}</div>`
|
text: `${img}<div class="noty-text">${text}</div>`
|
||||||
}).show();
|
}).show();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.statusClass = function (status) {
|
$app.methods.statusClass = function (status) {
|
||||||
var style = {};
|
let style = {};
|
||||||
if (typeof status === 'undefined') {
|
if (typeof status === 'undefined') {
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
@@ -593,56 +654,56 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
$app.data.cleanHudFeedLoopStatus = false;
|
$app.data.cleanHudFeedLoopStatus = false;
|
||||||
|
|
||||||
$app.methods.cleanHudFeedLoop = function () {
|
$app.methods.cleanHudFeedLoop = function () {
|
||||||
if (!this.cleanHudFeedLoopStatus) {
|
if (!$app.cleanHudFeedLoopStatus) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.cleanHudFeed();
|
$app.cleanHudFeed();
|
||||||
if (this.hudFeed.length === 0) {
|
if ($app.hudFeed.length === 0) {
|
||||||
this.cleanHudFeedLoopStatus = false;
|
$app.cleanHudFeedLoopStatus = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
workerTimers.setTimeout(() => this.cleanHudFeedLoop(), 500);
|
workerTimers.setTimeout(() => $app.cleanHudFeedLoop(), 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.cleanHudFeed = function () {
|
$app.methods.cleanHudFeed = function () {
|
||||||
var dt = Date.now();
|
const dt = Date.now();
|
||||||
this.hudFeed.forEach((item) => {
|
$app.hudFeed.forEach((item) => {
|
||||||
if (item.time + this.config.photonOverlayMessageTimeout < dt) {
|
if (item.time + $app.config.photonOverlayMessageTimeout < dt) {
|
||||||
removeFromArray(this.hudFeed, item);
|
removeFromArray($app.hudFeed, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.hudFeed.length > 10) {
|
if ($app.hudFeed.length > 10) {
|
||||||
this.hudFeed.length = 10;
|
$app.hudFeed.length = 10;
|
||||||
}
|
}
|
||||||
if (!this.cleanHudFeedLoopStatus) {
|
if (!$app.cleanHudFeedLoopStatus) {
|
||||||
this.cleanHudFeedLoopStatus = true;
|
$app.cleanHudFeedLoopStatus = true;
|
||||||
this.cleanHudFeedLoop();
|
$app.cleanHudFeedLoop();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.addEntryHudFeed = function (json) {
|
$app.methods.addEntryHudFeed = function (json) {
|
||||||
var data = JSON.parse(json);
|
const data = JSON.parse(json);
|
||||||
var combo = 1;
|
let combo = 1;
|
||||||
this.hudFeed.forEach((item) => {
|
$app.hudFeed.forEach((item) => {
|
||||||
if (
|
if (
|
||||||
item.displayName === data.displayName &&
|
item.displayName === data.displayName &&
|
||||||
item.text === data.text
|
item.text === data.text
|
||||||
) {
|
) {
|
||||||
combo = item.combo + 1;
|
combo = item.combo + 1;
|
||||||
removeFromArray(this.hudFeed, item);
|
removeFromArray($app.hudFeed, item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.hudFeed.unshift({
|
$app.hudFeed.unshift({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
combo,
|
combo,
|
||||||
...data
|
...data
|
||||||
});
|
});
|
||||||
this.cleanHudFeed();
|
$app.cleanHudFeed();
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.updateHudFeedTag = function (json) {
|
$app.methods.updateHudFeedTag = function (json) {
|
||||||
var ref = JSON.parse(json);
|
const ref = JSON.parse(json);
|
||||||
this.hudFeed.forEach((item) => {
|
$app.hudFeed.forEach((item) => {
|
||||||
if (item.userId === ref.userId) {
|
if (item.userId === ref.userId) {
|
||||||
item.colour = ref.colour;
|
item.colour = ref.colour;
|
||||||
}
|
}
|
||||||
@@ -652,16 +713,16 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
$app.data.hudTimeout = [];
|
$app.data.hudTimeout = [];
|
||||||
|
|
||||||
$app.methods.updateHudTimeout = function (json) {
|
$app.methods.updateHudTimeout = function (json) {
|
||||||
this.hudTimeout = JSON.parse(json);
|
$app.hudTimeout = JSON.parse(json);
|
||||||
};
|
};
|
||||||
|
|
||||||
$app.methods.setDatetimeFormat = async function () {
|
$app.methods.setDatetimeFormat = async function () {
|
||||||
this.currentCulture = await AppApiVr.CurrentCulture();
|
$app.currentCulture = await AppApiVr.CurrentCulture();
|
||||||
var formatDate = function (date) {
|
const formatDate = function (date) {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
var dt = new Date(date);
|
const dt = new Date(date);
|
||||||
return dt
|
return dt
|
||||||
.toLocaleTimeString($app.currentCulture, {
|
.toLocaleTimeString($app.currentCulture, {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
@@ -678,9 +739,9 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
if (!appLanguage) {
|
if (!appLanguage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (appLanguage !== this.appLanguage) {
|
if (appLanguage !== $app.appLanguage) {
|
||||||
this.appLanguage = appLanguage;
|
$app.appLanguage = appLanguage;
|
||||||
i18n.locale = this.appLanguage;
|
i18n.locale = $app.appLanguage;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -688,8 +749,8 @@ Vue.component('marquee-text', MarqueeText);
|
|||||||
if (document.contains(document.getElementById('app-emoji-font'))) {
|
if (document.contains(document.getElementById('app-emoji-font'))) {
|
||||||
document.getElementById('app-emoji-font').remove();
|
document.getElementById('app-emoji-font').remove();
|
||||||
}
|
}
|
||||||
if (this.isRunningUnderWine) {
|
if ($app.isRunningUnderWine) {
|
||||||
var $appEmojiFont = document.createElement('link');
|
const $appEmojiFont = document.createElement('link');
|
||||||
$appEmojiFont.setAttribute('id', 'app-emoji-font');
|
$appEmojiFont.setAttribute('id', 'app-emoji-font');
|
||||||
$appEmojiFont.rel = 'stylesheet';
|
$appEmojiFont.rel = 'stylesheet';
|
||||||
$appEmojiFont.href = 'emoji.font.css';
|
$appEmojiFont.href = 'emoji.font.css';
|
||||||
|
|||||||
17
src/vr.scss
17
src/vr.scss
@@ -8,7 +8,7 @@
|
|||||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||||
//
|
//
|
||||||
|
|
||||||
@use "./assets/scss/flags.scss";
|
@use './assets/scss/flags.scss';
|
||||||
|
|
||||||
@import '~animate.css/animate.min.css';
|
@import '~animate.css/animate.min.css';
|
||||||
@import '~noty/lib/noty.css';
|
@import '~noty/lib/noty.css';
|
||||||
@@ -20,6 +20,10 @@
|
|||||||
손등 18px -> 나나 24
|
손등 18px -> 나나 24
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.noty_body {
|
.noty_body {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -171,13 +175,20 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'ellipsis-font';
|
||||||
|
src: local('Times New Roman');
|
||||||
|
unicode-range: U+2026;
|
||||||
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
input,
|
input,
|
||||||
textarea,
|
textarea,
|
||||||
select,
|
select,
|
||||||
button {
|
button {
|
||||||
font-family: 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC', 'Noto Sans SC',
|
font-family:
|
||||||
'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
|
'ellipsis-font', 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC',
|
||||||
|
'Noto Sans SC', 'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
text-shadow:
|
text-shadow:
|
||||||
#000 0px 0px 3px,
|
#000 0px 0px 3px,
|
||||||
|
|||||||
Reference in New Issue
Block a user