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:
rs189
2025-07-19 09:07:43 +09:00
committed by GitHub
parent 53723d37b0
commit a2dc6ba9a4
53 changed files with 10555 additions and 7865 deletions

View File

@@ -112,6 +112,38 @@ jobs:
name: Electron-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:
runs-on: ubuntu-latest
needs:
@@ -122,7 +154,7 @@ jobs:
- name: Set version
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
run: sudo apt update && sudo apt install -y p7zip-full nsis nsis-pluginapi
- name: Set plugin permissions

View File

@@ -1,43 +1,41 @@
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using CefSharp;
namespace VRCX
{
public class AppApiVr
public class AppApiVrCef : AppApiVr
{
public static readonly AppApiVr Instance;
static AppApiVr()
static AppApiVrCef()
{
Instance = new AppApiVr();
Instance = new AppApiVrCef();
}
public void Init()
public override void Init()
{
// 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)
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>
/// Returns the current CPU usage as a percentage.
/// </summary>
/// <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>
@@ -45,7 +43,7 @@ namespace VRCX
/// Each sub-array contains the type of device and its current state
/// </summary>
/// <returns>An array of arrays containing information about the connected VR devices.</returns>
public string[][] GetVRDevices()
public override string[][] GetVRDevices()
{
return Program.VRCXVRInstance.GetDevices();
}
@@ -54,36 +52,42 @@ namespace VRCX
/// Returns the number of milliseconds that the system has been running.
/// </summary>
/// <returns>The number of milliseconds that the system has been running.</returns>
public double GetUptime()
public override double GetUptime()
{
return SystemMonitor.Instance.UpTime;
return SystemMonitorCef.Instance.UpTime;
}
/// <summary>
/// Returns the current language of the operating system.
/// </summary>
/// <returns>The current language of the operating system.</returns>
public string CurrentCulture()
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 string CustomVrScriptPath()
public override string CustomVrScript()
{
var output = string.Empty;
var filePath = Path.Join(Program.AppDataDirectory, "customvr.js");
if (File.Exists(filePath))
output = filePath;
return output;
return File.ReadAllText(filePath);
return string.Empty;
}
public bool IsRunningUnderWine()
public override bool IsRunningUnderWine()
{
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.");
}
}
}

View File

@@ -96,22 +96,22 @@ namespace VRCX
});
}
public string CustomCssPath()
public string CustomCss()
{
var output = string.Empty;
var filePath = Path.Join(Program.AppDataDirectory, "custom.css");
if (File.Exists(filePath))
output = filePath;
return output;
return File.ReadAllText(filePath);
return string.Empty;
}
public string CustomScriptPath()
public string CustomScript()
{
var output = string.Empty;
var filePath = Path.Join(Program.AppDataDirectory, "custom.js");
if (File.Exists(filePath))
output = filePath;
return output;
return File.ReadAllText(filePath);
return string.Empty;
}
public string CurrentCulture()

View 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();
}

View File

@@ -9,27 +9,30 @@ namespace VRCX
public partial class AppApiElectron : AppApi
{
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
public override void ShowDevTools()
{
}
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()
{
Program.VRCXVRInstance.Restart();
}
public override void RestartVR()
{
Program.VRCXVRInstance.Restart();
}
public override void SetZoom(double zoomLevel)
{
}
public override async Task<double> GetZoom()
{
return 1;
@@ -47,23 +50,25 @@ namespace VRCX
{
return false;
}
public override void ExecuteAppFunction(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)
{
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
}
public override void FocusWindow()
{
}
public override void ChangeTheme(int value)
{
}
@@ -71,7 +76,7 @@ namespace VRCX
public override void DoFunny()
{
}
public override string GetClipboard()
{
var process = new Process
@@ -102,7 +107,7 @@ namespace VRCX
public override void SetStartup(bool enabled)
{
}
public override void CopyImageToClipboard(string path)
{
if (!File.Exists(path) ||
@@ -113,7 +118,7 @@ namespace VRCX
!path.EndsWith(".bmp") &&
!path.EndsWith(".webp")))
return;
var process = new Process
{
StartInfo = new ProcessStartInfo
@@ -124,7 +129,7 @@ namespace VRCX
CreateNoWindow = true
}
};
try
try
{
process.Start();
process.WaitForExit();
@@ -138,16 +143,16 @@ namespace VRCX
public override void FlashWindow()
{
}
public override void SetUserAgent()
{
}
public override bool IsRunningUnderWine()
{
return false;
}
}
}

View 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;
}
}
}

View File

@@ -45,8 +45,7 @@ namespace VRCX
),
IsLocal = true
});
// cefSettings.CefCommandLineArgs.Add("allow-universal-access-from-files");
// cefSettings.CefCommandLineArgs.Add("ignore-certificate-errors");
// cefSettings.CefCommandLineArgs.Add("disable-plugins");
cefSettings.CefCommandLineArgs.Add("disable-spell-checking");

View File

@@ -15,7 +15,7 @@ namespace VRCX
repository.Register("Discord", Discord.Instance);
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
}
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
{
repository.NameConverter = null;

View File

@@ -18,7 +18,7 @@ using CefSharp;
namespace VRCX
{
internal class IPCClient
public class IPCClient
{
private static readonly UTF8Encoding noBomEncoding = new UTF8Encoding(false, false);
private readonly NamedPipeServerStream _ipcServer;

View File

@@ -11,7 +11,7 @@ using System.Threading.Tasks;
namespace VRCX
{
internal class IPCServer
public class IPCServer
{
public static readonly IPCServer Instance;
public static readonly List<IPCClient> Clients = new List<IPCClient>();

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,9 @@ using NLog;
namespace VRCX
{
public class SystemMonitor
public class SystemMonitorCef
{
public static readonly SystemMonitor Instance;
public static readonly SystemMonitorCef Instance;
public float CpuUsage;
public double UpTime;
private bool _enabled;
@@ -22,9 +22,9 @@ namespace VRCX
private Thread _thread;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
static SystemMonitor()
static SystemMonitorCef()
{
Instance = new SystemMonitor();
Instance = new SystemMonitorCef();
}
public void Start(bool enabled)

View File

@@ -5,6 +5,7 @@
// 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;
@@ -24,7 +25,7 @@ using Device4 = SharpDX.Direct3D11.Device4;
namespace VRCX
{
public class VRCXVR : VRCXVRInterface
public class VRCXVRCef : VRCXVRInterface
{
public static VRCXVRInterface Instance;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
@@ -57,12 +58,12 @@ namespace VRCX
private bool _wristOverlayWasActive;
static VRCXVR()
static VRCXVRCef()
{
Instance = new VRCXVR();
Instance = new VRCXVRCef();
}
public VRCXVR()
public VRCXVRCef()
{
_deviceListLock = new ReaderWriterLockSlim();
_deviceList = new List<string[]>();
@@ -90,7 +91,7 @@ namespace VRCX
public override void Restart()
{
Exit();
Instance = new VRCXVR();
Instance = new VRCXVRCef();
Instance.Init();
MainForm.Instance.Browser.ExecuteScriptAsync("console.log('VRCXVR Restarted');");
}
@@ -834,6 +835,11 @@ namespace VRCX
return err;
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
{
throw new NotImplementedException();
}
public override void ExecuteVrFeedFunction(string function, string json)
{
if (_wristOverlay == null) return;
@@ -842,6 +848,11 @@ namespace VRCX
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
{
throw new NotImplementedException();
}
public override void ExecuteVrOverlayFunction(string function, string json)
{
if (_hmdOverlay == null) return;

View File

@@ -5,6 +5,7 @@
// 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;
@@ -761,6 +762,11 @@ namespace VRCX
return err;
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrFeedFunctionQueue()
{
throw new NotImplementedException();
}
public override void ExecuteVrFeedFunction(string function, string json)
{
@@ -769,6 +775,11 @@ namespace VRCX
Restart();
_wristOverlay.ExecuteScriptAsync($"$app.{function}", json);
}
public override ConcurrentQueue<KeyValuePair<string, string>> GetExecuteVrOverlayFunctionQueue()
{
throw new NotImplementedException();
}
public override void ExecuteVrOverlayFunction(string function, string json)
{

View 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;
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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));
}
}
}

View File

@@ -1,9 +1,11 @@
namespace VRCX;
using System.Collections.Generic;
using System.Collections.Concurrent;
public abstract class VRCXVRInterface
{
public bool IsHmdAfk;
public abstract void Init();
public abstract void Exit();
public abstract void Refresh();
@@ -12,4 +14,6 @@ public abstract class VRCXVRInterface
public abstract string[][] GetDevices();
public abstract void ExecuteVrFeedFunction(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();
}

View File

@@ -24,10 +24,9 @@ namespace VRCX
public static string Version { get; private set; }
public static bool LaunchDebug;
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
#if !LINUX
public static VRCXVRInterface VRCXVRInstance { get; private set; }
#endif
public static AppApi AppApiInstance { get; private set; }
public static AppApiVr AppApiVrInstance { get; private set; }
private static void SetProgramDirectories()
{
@@ -74,7 +73,7 @@ namespace VRCX
try
{
var versionFile = File.ReadAllText(Path.Join(BaseDirectory, "Version")).Trim();
// look for trailing git hash "-22bcd96" to indicate nightly build
var version = versionFile.Split('-');
if (version.Length > 0 && version[^1].Length == 7)
@@ -234,7 +233,8 @@ namespace VRCX
SQLiteLegacy.Instance.Init();
AppApiInstance = new AppApiCef();
AppApiVr.Instance.Init();
AppApiVrInstance = new AppApiVrCef();
AppApiVrInstance.Init();
ProcessMonitor.Instance.Init();
Discord.Instance.Init();
WebApi.Instance.Init();
@@ -246,7 +246,7 @@ namespace VRCX
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
VRCXVRInstance = new VRCXVRLegacy();
else
VRCXVRInstance = new VRCXVR();
VRCXVRInstance = new VRCXVRCef();
VRCXVRInstance.Init();
Application.Run(new MainForm());
@@ -260,7 +260,7 @@ namespace VRCX
WebApi.Instance.Exit();
Discord.Instance.Exit();
SystemMonitor.Instance.Exit();
SystemMonitorCef.Instance.Exit();
VRCXStorage.Instance.Save();
SQLiteLegacy.Instance.Exit();
ProcessMonitor.Instance.Exit();
@@ -285,6 +285,9 @@ namespace VRCX
AppApiInstance = new AppApiElectron();
// ProcessMonitor.Instance.Init();
VRCXVRInstance = new VRCXVRElectron();
VRCXVRInstance.Init();
}
#endif
}

View File

@@ -121,8 +121,8 @@
<Compile Remove="Cef\**" />
<Content Remove="AppApi\Cef\**" />
<Compile Remove="AppApi\Cef\**" />
<Content Remove="Overlay\**" />
<Compile Remove="Overlay\**" />
<Content Remove="Overlay\Cef\**" />
<Compile Remove="Overlay\Cef\**" />
<Content Remove="DBMerger\**" />
<Compile Remove="DBMerger\**" />
</ItemGroup>

View File

@@ -94,6 +94,7 @@ namespace VRCX
catch (UriFormatException)
{
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.";
#if !LINUX
System.Windows.Forms.MessageBox.Show(message, "Invalid Proxy URI", MessageBoxButtons.OK, MessageBoxIcon.Error);

BIN
VRCX.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -139,10 +139,19 @@
},
"maintainer": "rs189 <35667100+rs189@users.noreply.github.com>",
"description": "Friendship management tool for VRChat"
},
"mac": {
"artifactName": "VRCX_Version.${ext}",
"target": [
"dmg"
],
"icon": "VRCX.png",
"category": "public.app-category.utilities",
"executableName": "VRCX"
}
},
"dependencies": {
"hazardous": "^0.3.0",
"node-api-dotnet": "^0.9.12"
}
}
}

View File

@@ -5,88 +5,101 @@ const { spawnSync } = require('child_process');
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_DIR = path.join(__dirname, '..', 'build', 'Electron', 'dotnet-runtime');
const DOTNET_BIN_DIR = path.join(DOTNET_RUNTIME_DIR, 'bin');
const DOTNET_RUNTIME_DIR = path.join(
__dirname,
'..',
'build',
'Electron',
'dotnet-runtime'
);
async function downloadFile(url, targetPath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(targetPath);
https.get(url, (response) => {
if (response.statusCode !== 200) {
reject(new Error(`Failed to download, status code: ${response.statusCode}`));
return;
}
response.pipe(file);
file.on('finish', () => {
file.close(resolve);
https
.get(url, (response) => {
if (response.statusCode !== 200) {
reject(
new Error(
`Failed to download, status code: ${response.statusCode}`
)
);
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) {
return new Promise((resolve, reject) => {
const tar = spawnSync('tar', ['-xzf', tarGzPath, '-C', extractDir, '--strip-components=1'], {
stdio: 'inherit'
});
const tar = spawnSync(
'tar',
['-xzf', tarGzPath, '-C', extractDir, '--strip-components=1'],
{
stdio: 'inherit'
}
);
if (tar.status === 0) {
resolve();
} else {
reject(new Error(`tar extraction failed with status ${tar.status}`));
reject(
new Error(`tar extraction failed with status ${tar.status}`)
);
}
});
}
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...`);
if (!fs.existsSync(DOTNET_RUNTIME_DIR)) {
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');
try {
// Download .NET runtime
await downloadFile(DOTNET_RUNTIME_URL, tarGzPath);
console.log('Download completed');
// Extract .NET runtime to a temporary directory first
const tempExtractDir = path.join(DOTNET_RUNTIME_DIR, 'temp');
if (!fs.existsSync(tempExtractDir)) {
fs.mkdirSync(tempExtractDir, { recursive: true });
}
console.log('Extracting .NET runtime...');
await extractTarGz(tarGzPath, tempExtractDir);
console.log('Extraction completed');
// Clean up tar.gz file
fs.unlinkSync(tarGzPath);
console.log('Cleanup completed');
// Move dotnet executable to bin directory
// Ensure the dotnet executable is executable
const extractedDotnet = path.join(tempExtractDir, 'dotnet');
const targetDotnet = path.join(DOTNET_BIN_DIR, 'dotnet');
if (fs.existsSync(extractedDotnet)) {
fs.renameSync(extractedDotnet, targetDotnet);
fs.chmodSync(targetDotnet, 0o755);
console.log('Moved dotnet executable to bin directory');
}
fs.chmodSync(extractedDotnet, 0o755);
// Move all other files to the root of dotnet-runtime
const files = fs.readdirSync(tempExtractDir);
for (const file of files) {
const sourcePath = path.join(tempExtractDir, file);
const targetPath = path.join(DOTNET_RUNTIME_DIR, file);
if (fs.existsSync(targetPath)) {
if (fs.lstatSync(sourcePath).isDirectory()) {
// Remove existing directory and move new one
@@ -96,16 +109,16 @@ async function main() {
fs.unlinkSync(targetPath);
}
}
fs.renameSync(sourcePath, targetPath);
}
// Clean up temp directory
fs.rmSync(tempExtractDir, { recursive: true, force: true });
console.log(`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`);
console.log(`dotnet executable available at: ${targetDotnet}`);
console.log(
`.NET runtime downloaded and extracted to: ${DOTNET_RUNTIME_DIR}`
);
} catch (error) {
console.error('Error:', error.message);
process.exit(1);
@@ -116,4 +129,4 @@ if (require.main === module) {
main();
}
module.exports = { downloadFile, extractTarGz };
module.exports = { downloadFile, extractTarGz };

View File

@@ -7,19 +7,35 @@ const {
Tray,
Menu,
dialog,
Notification
Notification,
nativeImage
} = require('electron');
const { spawn, spawnSync } = require('child_process');
const fs = require('fs');
const https = require('https');
// Include bundled .NET runtime
const bundledDotNetPath = path.join(process.resourcesPath, 'dotnet-runtime');
const bundledDotnet = path.join(bundledDotNetPath, 'bin', 'dotnet');
//app.disableHardwareAcceleration();
if (fs.existsSync(bundledDotnet)) {
process.env.DOTNET_ROOT = bundledDotNetPath;
process.env.PATH = `${path.dirname(bundledDotnet)}:${process.env.PATH}`;
if (process.platform === 'linux') {
// Include bundled .NET runtime
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()) {
@@ -30,9 +46,11 @@ if (!isDotNetInstalled()) {
);
app.quit();
});
return;
}
let isOverlayActive = false;
let appIsQuitting = false;
// Get launch arguments
let appImagePath = process.env.APPIMAGE;
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 = 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();
interopApi.getDotNetObject('ProgramElectron').PreInit(version, args);
interopApi.getDotNetObject('VRCXStorage').Load();
@@ -61,6 +97,10 @@ interopApi.getDotNetObject('Discord').Init();
interopApi.getDotNetObject('WebApi').Init();
interopApi.getDotNetObject('LogWatcher').Init();
interopApi.getDotNetObject('IPCServer').Init();
interopApi.getDotNetObject('SystemMonitorElectron').Init();
interopApi.getDotNetObject('AppApiVrElectron').Init();
ipcMain.handle('callDotNetMethod', (event, 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) {
if (
process.platform !== 'linux' ||
@@ -188,14 +267,11 @@ function createWindow() {
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
},
webContents: {
userAgent: version
}
});
applyWindowState();
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
// const proxy = VRCXStorage.Get('VRCX_Proxy');
@@ -234,7 +310,7 @@ function createWindow() {
mainWindow.on('close', (event) => {
isCloseToTray = VRCXStorage.Get('VRCX_CloseToTray') === 'true';
if (isCloseToTray && !app.isQuitting) {
if (isCloseToTray && !appIsQuitting) {
event.preventDefault();
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() {
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([
{
label: 'Open',
@@ -296,7 +506,7 @@ function createTray() {
label: 'Quit VRCX',
type: 'normal',
click: function () {
app.isQuitting = true;
appIsQuitting = true;
app.quit();
}
}
@@ -507,18 +717,19 @@ function getHomePath() {
const absoluteHomePath = fs.realpathSync(relativeHomePath);
return absoluteHomePath;
} catch (err) {
console.error('Error resolving absolute home path:', err);
return relativeHomePath;
}
}
function getVersion() {
try {
var versionFile = fs
const versionFile = fs
.readFileSync(path.join(rootDir, 'Version'), 'utf8')
.trim();
// look for trailing git hash "-22bcd96" to indicate nightly build
var version = versionFile.split('-');
const version = versionFile.split('-');
console.log('Version:', versionFile);
if (version.length > 0 && version[version.length - 1].length == 7) {
return `VRCX (Linux) Nightly ${versionFile}`;
@@ -532,19 +743,15 @@ function getVersion() {
}
function isDotNetInstalled() {
if (process.platform === 'darwin') {
// Assume .NET is already installed on macOS
return true;
let dotnetPath = path.join(process.env.DOTNET_ROOT, 'dotnet');
if (!process.env.DOTNET_ROOT || !fs.existsSync(dotnetPath)) {
// fallback to command
dotnetPath = 'dotnet';
}
// Check for bundled .NET runtime
if (fs.existsSync(bundledDotnet)) {
console.log('Using bundled .NET runtime at:', bundledDotNetPath);
return true;
}
console.log('Checking for .NET installation at:', dotnetPath);
// Fallback to system .NET runtime
const result = spawnSync('dotnet', ['--list-runtimes'], {
const result = spawnSync(dotnetPath, ['--list-runtimes'], {
encoding: 'utf-8'
});
return result.stdout?.includes('.NETCore.App 9.0');
@@ -611,20 +818,54 @@ function applyWindowState() {
app.whenReady().then(() => {
createWindow();
createTray();
if (process.platform === 'linux') {
createWristOverlayWindowOffscreen();
createHmdOverlayWindowOffscreen();
}
installVRCX();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
// app.on('before-quit', function () {
// mainWindow.webContents.send('windowClosed');
// });
function disposeOverlay() {
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 () {
if (process.platform !== 'darwin') app.quit();
disposeOverlay();
if (process.platform !== 'darwin') {
app.quit();
}
});

View File

@@ -0,0 +1,5 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateImage: (callback) => ipcRenderer.on('update-image', (event, base64) => callback(base64))
});

View 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>

View File

@@ -31,11 +31,17 @@ managedHostPath = managedHostPath.indexOf('app.asar.unpacked') < 0 ?
}
// Paths to patch
let platformName = 'linux';
let platformName = '';
switch (process.platform) {
case 'win32':
platformName = 'win';
break;
case 'darwin':
platformName = 'mac';
break;
case 'linux':
platformName = 'linux';
break;
}
const postBuildPath = path.join(
__dirname,

View File

@@ -30,5 +30,9 @@ contextBridge.exposeInMainWorld('electron', {
ipcRenderer.on('setWindowState', callback),
desktopNotification: (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)
});

View File

@@ -17,17 +17,34 @@ try {
process.exit(1);
}
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
try {
if (fs.existsSync(oldAppImage)) {
fs.renameSync(oldAppImage, newAppImage);
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
} else {
console.log(`File not found: ${oldAppImage}`);
if (process.platform === 'linux') {
const oldAppImage = path.join(buildDir, `VRCX_Version.AppImage`);
const newAppImage = path.join(buildDir, `VRCX_${version}.AppImage`);
try {
if (fs.existsSync(oldAppImage)) {
fs.renameSync(oldAppImage, newAppImage);
console.log(`Renamed: ${oldAppImage} -> ${newAppImage}`);
} else {
console.log(`File not found: ${oldAppImage}`);
}
} catch (err) {
console.error('Error renaming files:', err);
process.exit(1);
}
} catch (err) {
console.error('Error renaming files:', err);
process.exit(1);
} else if (process.platform === 'darwin') {
const oldDmg = path.join(buildDir, `VRCX_Version.dmg`);
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.');
}

View File

@@ -20,4 +20,5 @@ if (WINDOWS) {
window.LogWatcher = InteropApi.LogWatcher;
window.Discord = InteropApi.Discord;
window.AssetBundleManager = InteropApi.AssetBundleManager;
}
window.AppApiVrElectron = InteropApi.AppApiVrElectron;
}

View File

@@ -151,35 +151,35 @@ function updateTrustColorClasses(trustColor) {
document.getElementsByTagName('head')[0].appendChild(style);
}
function refreshCustomCss() {
async function refreshCustomCss() {
if (document.contains(document.getElementById('app-custom-style'))) {
document.getElementById('app-custom-style').remove();
}
AppApi.CustomCssPath().then((customCss) => {
const customCss = await AppApi.CustomCss();
if (customCss) {
const head = document.head;
if (customCss) {
const $appCustomStyle = document.createElement('link');
$appCustomStyle.setAttribute('id', 'app-custom-style');
$appCustomStyle.rel = 'stylesheet';
$appCustomStyle.href = `file://${customCss}?_=${Date.now()}`;
head.appendChild($appCustomStyle);
}
});
const $appCustomStyle = document.createElement('link');
$appCustomStyle.setAttribute('id', 'app-custom-style');
$appCustomStyle.rel = 'stylesheet';
$appCustomStyle.type = 'text/css';
$appCustomStyle.textContent = customCss;
head.appendChild($appCustomStyle);
}
}
function refreshCustomScript() {
async function refreshCustomScript() {
if (document.contains(document.getElementById('app-custom-script'))) {
document.getElementById('app-custom-script').remove();
}
AppApi.CustomScriptPath().then((customScript) => {
const customScript = await AppApi.CustomScript();
if (customScript) {
const head = document.head;
if (customScript) {
const $appCustomScript = document.createElement('script');
$appCustomScript.setAttribute('id', 'app-custom-script');
$appCustomScript.src = `file://${customScript}?_=${Date.now()}`;
head.appendChild($appCustomScript);
}
});
const $appCustomScript = document.createElement('script');
$appCustomScript.setAttribute('id', 'app-custom-script');
$appCustomScript.type = 'text/javascript';
$appCustomScript.textContent = customScript;
head.appendChild($appCustomScript);
}
}
/**

View File

@@ -60,7 +60,7 @@ function parseLocation(tag) {
ctx.isPrivate = true;
} else if (_tag === 'traveling' || _tag === 'traveling:traveling') {
ctx.isTraveling = true;
} else if (!_tag.startsWith('local')) {
} else if (tag && !_tag.startsWith('local')) {
ctx.isRealInstance = true;
const sep = _tag.indexOf(':');
// technically not part of instance id, but might be there when coping id from url so why not support it

View File

@@ -385,31 +385,35 @@ export const useAvatarStore = defineStore('Avatar', () => {
}
/**
* aka: `$app.methods.addAvatarToHistory`
* @param {string} avatarId
*/
function addAvatarToHistory(avatarId) {
avatarRequest.getAvatar({ avatarId }).then((args) => {
const ref = applyAvatar(args.json);
avatarRequest
.getAvatar({ avatarId })
.then((args) => {
const ref = applyAvatar(args.json);
database.addAvatarToCache(ref);
database.addAvatarToHistory(ref.id);
database.addAvatarToCache(ref);
database.addAvatarToHistory(ref.id);
if (ref.authorId === userStore.currentUser.id) {
return;
}
const historyArray = state.avatarHistoryArray;
for (let i = 0; i < historyArray.length; ++i) {
if (historyArray[i].id === ref.id) {
historyArray.splice(i, 1);
if (ref.authorId === userStore.currentUser.id) {
return;
}
}
state.avatarHistoryArray.unshift(ref);
state.avatarHistory.delete(ref.id);
state.avatarHistory.add(ref.id);
});
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);
state.avatarHistory.delete(ref.id);
state.avatarHistory.add(ref.id);
})
.catch((err) => {
console.error('Failed to add avatar to history:', err);
});
}
function clearAvatarHistory() {

View File

@@ -531,10 +531,10 @@ export const useAdvancedSettingsStore = defineStore('AdvancedSettings', () => {
state.folderSelectorDialogVisible = true;
let newFolder = '';
if (LINUX) {
newFolder = await window.electron.openDirectoryDialog();
if (WINDOWS) {
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
} else {
newFolder = await AppApi.OpenFolderSelectorDialog(oldPath);
newFolder = await window.electron.openDirectoryDialog();
}
state.folderSelectorDialogVisible = false;

View File

@@ -362,10 +362,10 @@ export const useNotificationsSettingsStore = defineStore(
function getTTSVoiceName() {
let voices;
if (LINUX) {
voices = state.TTSvoices;
} else {
if (WINDOWS) {
voices = speechSynthesis.getVoices();
} else {
voices = state.TTSvoices;
}
if (voices.length === 0) {
return '';
@@ -379,10 +379,10 @@ export const useNotificationsSettingsStore = defineStore(
async function changeTTSVoice(index) {
setNotificationTTSVoice(index);
let voices;
if (LINUX) {
voices = state.TTSvoices;
} else {
if (WINDOWS) {
voices = speechSynthesis.getVoices();
} else {
voices = state.TTSvoices;
}
if (voices.length === 0) {
return;

View File

@@ -133,16 +133,16 @@ export const useUpdateLoopStore = defineStore('UpdateLoop', () => {
(vrcxStore.isRunningUnderWine || LINUX) &&
--state.nextGameRunningCheck <= 0
) {
if (LINUX) {
if (WINDOWS) {
state.nextGameRunningCheck = 3;
AppApi.CheckGameRunning();
} else {
state.nextGameRunningCheck = 1;
gameStore.updateIsGameRunning(
await AppApi.IsGameRunning(),
await AppApi.IsSteamVRRunning(),
false
);
} else {
state.nextGameRunningCheck = 3;
AppApi.CheckGameRunning();
}
}
if (--state.nextDatabaseOptimize <= 0) {

View File

@@ -851,12 +851,7 @@ export const useUserStore = defineStore('User', () => {
}
applyUserDialogLocation(true);
if (
args.cache &&
args.ref.$lastFetch < Date.now() - 10000 // 10 seconds
) {
userRequest.getUser(args.params);
}
userRequest.getUser(args.params);
let inCurrentWorld = false;
if (
locationStore.lastLocation.playerList.has(D.ref.id)

View File

@@ -27,7 +27,9 @@ export const useVrStore = defineStore('Vr', () => {
const userStore = useUserStore();
const sharedFeedStore = useSharedFeedStore();
const state = reactive({});
const state = reactive({
overlayActive: false
});
watch(
() => watchState.isFriendsLoaded,
@@ -48,6 +50,8 @@ export const useVrStore = defineStore('Vr', () => {
sharedFeedStore.updateSharedFeed(true);
friendStore.onlineFriendCount = 0; // force an update
friendStore.updateOnlineFriendCoutner();
state.overlayActive = true;
}
async function saveOpenVROption() {
@@ -74,7 +78,7 @@ export const useVrStore = defineStore('Vr', () => {
}
}
}
let onlineFor = '';
let onlineFor = null;
if (!wristOverlaySettingsStore.hideUptimeFromFeed) {
onlineFor = userStore.currentUser.$online_for;
}
@@ -125,6 +129,13 @@ export const useVrStore = defineStore('Vr', () => {
}
function updateOpenVR() {
let newState = {
active: false,
hmdOverlay: false,
wristOverlay: false,
menuButton: false,
overlayHand: 0
};
if (
notificationsSettingsStore.openVR &&
gameStore.isSteamVRRunning &&
@@ -140,16 +151,42 @@ export const useVrStore = defineStore('Vr', () => {
) {
hmdOverlay = true;
}
// active, hmdOverlay, wristOverlay, menuButton, overlayHand
AppApi.SetVR(
true,
newState = {
active: true,
hmdOverlay,
wristOverlaySettingsStore.overlayWrist,
wristOverlaySettingsStore.overlaybutton,
wristOverlaySettingsStore.overlayHand
wristOverlay: wristOverlaySettingsStore.overlayWrist,
menuButton: wristOverlaySettingsStore.overlaybutton,
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
}
}
}

View File

@@ -652,11 +652,11 @@ export const useVrcxStore = defineStore('Vrcx', () => {
async function backupVrcRegistry(name) {
let regJson;
if (LINUX) {
if (WINDOWS) {
regJson = await AppApi.GetVRChatRegistry();
} else {
regJson = await AppApi.GetVRChatRegistryJson();
regJson = JSON.parse(regJson);
} else {
regJson = await AppApi.GetVRChatRegistry();
}
const newBackup = {
name,

View File

@@ -7,6 +7,7 @@ declare global {
interface Window {
$app: any;
AppApi: AppApi;
AppApiVr: AppApiVr;
WebApi: WebApi;
VRCXStorage: VRCXStorage;
SQLite: SQLite;
@@ -51,6 +52,15 @@ declare global {
Function: (event: any, state: { windowState: any }) => void
) => 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__: {
debug: boolean;
@@ -141,7 +151,10 @@ declare global {
};
const Discord: {
SetTimestamps(startTimestamp: number, endTimestamp: number): void;
SetTimestamps(
startTimestamp: number,
endTimestamp: number
): Promise<void>;
SetAssets(
bigIcon: string,
bigIconText: string,
@@ -154,8 +167,8 @@ declare global {
buttonUrl: string,
appId: string,
activityType: number
): void;
SetText(details: string, state: string): void;
): Promise<void>;
SetText(details: string, state: string): Promise<void>;
SetActive(active: boolean): Promise<boolean>;
};
@@ -202,8 +215,8 @@ declare global {
GetLaunchCommand(): Promise<string>;
IPCAnnounceStart(): Promise<void>;
SendIpc(type: string, data: string): Promise<void>;
CustomCssPath(): Promise<string>;
CustomScriptPath(): Promise<string>;
CustomCss(): Promise<string>;
CustomScript(): Promise<string>;
CurrentCulture(): Promise<string>;
CurrentLanguage(): Promise<string>;
GetVersion(): Promise<string>;
@@ -355,21 +368,23 @@ declare global {
};
const AppApiVr: {
Init(): void;
VrInit(): void;
ToggleSystemMonitor(enabled: boolean): void;
CpuUsage(): number;
GetVRDevices(): string[][];
GetUptime(): number;
CurrentCulture(): string;
CustomVrScriptPath(): string;
IsRunningUnderWine(): boolean;
Init(): Promise<void>;
VrInit(): Promise<void>;
ToggleSystemMonitor(enabled: boolean): Promise<void>;
CpuUsage(): Promise<number>;
GetVRDevices(): Promise<string[][]>;
GetUptime(): Promise<number>;
CurrentCulture(): Promise<string>;
CustomVrScript(): Promise<string>;
IsRunningUnderWine(): Promise<boolean>;
GetExecuteVrFeedFunctionQueue(): Promise<Map<string, string>>;
GetExecuteVrOverlayFunctionQueue(): Promise<Map<string, string>>;
};
const WebApi: {
ClearCookies(): void;
GetCookies(): string;
SetCookies(cookie: string): void;
ClearCookies(): Promise<void>;
GetCookies(): Promise<string>;
SetCookies(cookie: string): Promise<void>;
Execute(options: any): Promise<{ Item1: number; Item2: string }>;
ExecuteJson(requestJson: string): Promise<string>;
};
@@ -399,9 +414,9 @@ declare global {
};
const webApiService: {
clearCookies(): void;
getCookies(): string;
setCookies(cookie: string): void;
clearCookies(): Promise<void>;
getCookies(): Promise<string>;
setCookies(cookie: string): Promise<void>;
execute(options: {
url: string;
method: string;

2
src/types/user.d.ts vendored
View File

@@ -128,7 +128,7 @@ interface getCurrentUserResponse extends getUserResponse {
oculusId: string;
offlineFriends: string[];
onlineFriends: string[];
pastDisplayNames: { displayName: string; dateChanged: string }[];
pastDisplayNames: { displayName: string; updated_at: string }[];
picoId: string;
presence?: {
avatarThumbnail: string;

View File

@@ -595,6 +595,7 @@
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_user_id.input_error'),
callback: (action, instance) => {
instance.inputValue = instance.inputValue.trim();
if (action === 'confirm' && instance.inputValue) {
const testUrl = instance.inputValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {
@@ -622,6 +623,7 @@
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_world_id.input_error'),
callback: (action, instance) => {
instance.inputValue = instance.inputValue.trim();
if (action === 'confirm' && instance.inputValue) {
if (!directAccessWorld(instance.inputValue)) {
$message({
@@ -641,6 +643,7 @@
inputPattern: /\S+/,
inputErrorMessage: t('prompt.direct_access_avatar_id.input_error'),
callback: (action, instance) => {
instance.inputValue = instance.inputValue.trim();
if (action === 'confirm' && instance.inputValue) {
const testUrl = instance.inputValue.substring(0, 15);
if (testUrl === 'https://vrchat.') {

View File

@@ -77,32 +77,31 @@
<!--//- General | Application-->
<div class="options-container">
<span class="header">{{ t('view.settings.general.application.header') }}</span>
<template v-if="!isLinux">
<simple-switch
:label="t('view.settings.general.application.startup')"
:value="isStartAtWindowsStartup"
@change="setIsStartAtWindowsStartup" />
<simple-switch
:label="t('view.settings.general.application.minimized')"
:value="isStartAsMinimizedState"
@change="setIsStartAsMinimizedState" />
<simple-switch
:label="t('view.settings.general.application.tray')"
:value="isCloseToTray"
@change="setIsCloseToTray" />
</template>
<template v-if="!isLinux">
<simple-switch
:label="t('view.settings.general.application.disable_gpu_acceleration')"
:value="disableGpuAcceleration"
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
@change="setDisableGpuAcceleration" />
<simple-switch
:label="t('view.settings.general.application.disable_vr_overlay_gpu_acceleration')"
:value="disableVrOverlayGpuAcceleration"
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
@change="setDisableVrOverlayGpuAcceleration" />
</template>
<simple-switch
v-if="!isLinux"
:label="t('view.settings.general.application.startup')"
:value="isStartAtWindowsStartup"
@change="setIsStartAtWindowsStartup" />
<simple-switch
:label="t('view.settings.general.application.minimized')"
:value="isStartAsMinimizedState"
@change="setIsStartAsMinimizedState" />
<simple-switch
:label="t('view.settings.general.application.tray')"
:value="isCloseToTray"
@change="setIsCloseToTray" />
<simple-switch
v-if="!isLinux"
:label="t('view.settings.general.application.disable_gpu_acceleration')"
:value="disableGpuAcceleration"
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
@change="setDisableGpuAcceleration" />
<simple-switch
v-if="!isLinux"
:label="t('view.settings.general.application.disable_vr_overlay_gpu_acceleration')"
:value="disableVrOverlayGpuAcceleration"
:tooltip="t('view.settings.general.application.disable_gpu_acceleration_tooltip')"
@change="setDisableVrOverlayGpuAcceleration" />
<div class="options-container-item">
<el-button size="small" icon="el-icon-connection" @click="promptProxySettings">{{
t('view.settings.general.application.proxy')
@@ -796,7 +795,7 @@
}}</el-radio-button>
</el-radio-group>
</div>
<template v-if="!isLinux">
<template>
<simple-switch
:label="
t('view.settings.notifications.notifications.steamvr_notifications.steamvr_overlay')
@@ -831,6 +830,43 @@
}}</el-button
>
</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
:label="
t(
@@ -856,61 +892,30 @@
saveOpenVROption();
" />
</template>
<simple-switch
:label="
t(
'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"
>{{
<template v-if="!isLinux">
<simple-switch
:label="
t(
'view.settings.notifications.notifications.steamvr_notifications.notification_timeout'
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_hud_notifications'
)
}}</el-button
>
</div>
"
:value="ovrtHudNotifications"
@change="
setOvrtHudNotifications();
saveOpenVROption();
" />
<simple-switch
:label="
t(
'view.settings.notifications.notifications.steamvr_notifications.ovrtoolkit_wrist_notifications'
)
"
:value="ovrtWristNotifications"
@change="
setOvrtWristNotifications();
saveOpenVROption();
" />
</template>
</div>
<!--//- Notifications | Notifications | Desktop Notifications-->
<div class="options-container">
@@ -1047,7 +1052,7 @@
</el-tab-pane>
<!--//- 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-->
<div class="options-container" style="margin-top: 0">
<span class="header">{{ t('view.settings.wrist_overlay.steamvr_wrist_overlay.header') }}</span>
@@ -1436,19 +1441,15 @@
saveOpenVROption();
"></simple-switch>
<!--//- Advanced | VRChat Quit Fix-->
<template v-if="!isLinux">
<span class="sub-header">{{
t('view.settings.advanced.advanced.vrchat_quit_fix.header')
}}</span>
<simple-switch
:label="t('view.settings.advanced.advanced.vrchat_quit_fix.description')"
:value="vrcQuitFix"
:long-label="true"
@change="
setVrcQuitFix();
saveOpenVROption();
" />
</template>
<span class="sub-header">{{ t('view.settings.advanced.advanced.vrchat_quit_fix.header') }}</span>
<simple-switch
:label="t('view.settings.advanced.advanced.vrchat_quit_fix.description')"
:value="vrcQuitFix"
:long-label="true"
@change="
setVrcQuitFix();
saveOpenVROption();
" />
<!--//- Advanced | Auto Cache Management-->
<span class="sub-header">{{
t('view.settings.advanced.advanced.auto_cache_management.header')
@@ -1528,7 +1529,7 @@
</div>
</div>
<!--//- 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>
<simple-switch
:label="t('view.settings.advanced.advanced.video_progress_pie.enable')"

293
src/vr.js
View File

@@ -23,13 +23,19 @@ import { removeFromArray } from './shared/utils/base/array';
import { timeToText } from './shared/utils/base/format';
import pugTemplate from './vr.pug';
import InteropApi from './ipc-electron/interopApi.js';
Vue.component('marquee-text', MarqueeText);
(async function () {
let $app = {};
await CefSharp.BindObjectAsync('AppApiVr');
if (WINDOWS) {
await CefSharp.BindObjectAsync('AppApiVr');
} else {
// @ts-ignore
window.AppApiVr = InteropApi.AppApiVrElectron;
}
Noty.overrideDefaults({
animation: {
@@ -82,7 +88,7 @@ Vue.component('marquee-text', MarqueeText);
methods: {
parse() {
this.text = this.location;
var L = parseLocation(this.location);
const L = parseLocation(this.location);
if (L.isOffline) {
this.text = 'Offline';
} else if (L.isPrivate) {
@@ -176,9 +182,12 @@ Vue.component('marquee-text', MarqueeText);
watch: {},
el: '#root',
async mounted() {
this.isRunningUnderWine = await AppApiVr.IsRunningUnderWine();
await this.applyWineEmojis();
workerTimers.setTimeout(() => AppApiVr.VrInit(), 5000);
if (WINDOWS) {
this.isRunningUnderWine = await AppApiVr.IsRunningUnderWine();
await this.applyWineEmojis();
} else {
this.updateVrElectronLoop();
}
if (this.appType === '1') {
this.refreshCustomScript();
this.updateStatsLoop();
@@ -189,97 +198,97 @@ Vue.component('marquee-text', MarqueeText);
Object.assign($app, app);
$app.methods.configUpdate = function (json) {
this.config = JSON.parse(json);
this.hudFeed = [];
this.hudTimeout = [];
this.setDatetimeFormat();
this.setAppLanguage(this.config.appLanguage);
this.updateFeedLength();
$app.config = JSON.parse(json);
$app.hudFeed = [];
$app.hudTimeout = [];
$app.setDatetimeFormat();
$app.setAppLanguage($app.config.appLanguage);
$app.updateFeedLength();
if (
this.config.vrOverlayCpuUsage !== this.cpuUsageEnabled ||
this.config.pcUptimeOnFeed !== this.pcUptimeEnabled
$app.config.vrOverlayCpuUsage !== $app.cpuUsageEnabled ||
$app.config.pcUptimeOnFeed !== $app.pcUptimeEnabled
) {
this.cpuUsageEnabled = this.config.vrOverlayCpuUsage;
this.pcUptimeEnabled = this.config.pcUptimeOnFeed;
$app.cpuUsageEnabled = $app.config.vrOverlayCpuUsage;
$app.pcUptimeEnabled = $app.config.pcUptimeOnFeed;
AppApiVr.ToggleSystemMonitor(
this.cpuUsageEnabled || this.pcUptimeEnabled
$app.cpuUsageEnabled || $app.pcUptimeEnabled
);
}
if (this.config.notificationOpacity !== this.notificationOpacity) {
this.notificationOpacity = this.config.notificationOpacity;
this.setNotyOpacity(this.notificationOpacity);
if ($app.config.notificationOpacity !== $app.notificationOpacity) {
$app.notificationOpacity = $app.config.notificationOpacity;
$app.setNotyOpacity($app.notificationOpacity);
}
};
$app.methods.updateOnlineFriendCount = function (count) {
this.onlineFriendCount = parseInt(count, 10);
$app.onlineFriendCount = parseInt(count, 10);
};
$app.methods.nowPlayingUpdate = function (json) {
this.nowPlaying = JSON.parse(json);
if (this.appType === '2') {
var circle = document.querySelector('.np-progress-circle-stroke');
$app.nowPlaying = JSON.parse(json);
if ($app.appType === '2') {
const circle = document.querySelector('.np-progress-circle-stroke');
if (
this.lastLocation.progressPie &&
this.nowPlaying.percentage !== 0
$app.lastLocation.progressPie &&
$app.nowPlaying.percentage !== 0
) {
circle.style.opacity = 0.5;
var circumference = circle.getTotalLength();
const circumference = circle.getTotalLength();
circle.style.strokeDashoffset =
circumference -
(this.nowPlaying.percentage / 100) * circumference;
($app.nowPlaying.percentage / 100) * circumference;
} else {
circle.style.opacity = 0;
}
}
this.updateFeedLength();
$app.updateFeedLength();
};
$app.methods.lastLocationUpdate = function (json) {
this.lastLocation = JSON.parse(json);
$app.lastLocation = JSON.parse(json);
};
$app.methods.wristFeedUpdate = function (json) {
this.wristFeed = JSON.parse(json);
this.updateFeedLength();
$app.wristFeed = JSON.parse(json);
$app.updateFeedLength();
};
$app.methods.updateFeedLength = function () {
if (this.appType === '2' || this.wristFeed.length === 0) {
if ($app.appType === '2' || $app.wristFeed.length === 0) {
return;
}
var length = 16;
if (!this.config.hideDevicesFromFeed) {
let length = 16;
if (!$app.config.hideDevicesFromFeed) {
length -= 2;
if (this.deviceCount > 8) {
if ($app.deviceCount > 8) {
length -= 1;
}
}
if (this.nowPlaying.playing) {
if ($app.nowPlaying.playing) {
length -= 1;
}
if (length < this.wristFeed.length) {
this.wristFeed.length = length;
if (length < $app.wristFeed.length) {
$app.wristFeed.length = length;
}
};
$app.methods.refreshCustomScript = function () {
$app.methods.refreshCustomScript = async function () {
if (document.contains(document.getElementById('vr-custom-script'))) {
document.getElementById('vr-custom-script').remove();
}
AppApiVr.CustomVrScriptPath().then((customScript) => {
var head = document.head;
if (customScript) {
var $vrCustomScript = document.createElement('script');
$vrCustomScript.setAttribute('id', 'vr-custom-script');
$vrCustomScript.src = `file://${customScript}?_=${Date.now()}`;
head.appendChild($vrCustomScript);
}
});
const customScript = await AppApiVr.CustomVrScript();
if (customScript) {
const head = document.head;
const $vrCustomScript = document.createElement('script');
$vrCustomScript.setAttribute('id', 'vr-custom-script');
$vrCustomScript.type = 'text/javascript';
$vrCustomScript.textContent = customScript;
head.appendChild($vrCustomScript);
}
};
$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');
if (!element) {
document.body.insertAdjacentHTML(
@@ -293,43 +302,43 @@ Vue.component('marquee-text', MarqueeText);
$app.methods.updateStatsLoop = async function () {
try {
this.currentTime = new Date()
.toLocaleDateString(this.currentCulture, {
$app.currentTime = new Date()
.toLocaleDateString($app.currentCulture, {
month: '2-digit',
day: '2-digit',
year: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hourCycle: this.config.dtHour12 ? 'h12' : 'h23'
hourCycle: $app.config.dtHour12 ? 'h12' : 'h23'
})
.replace(' AM', ' am')
.replace(' PM', ' pm')
.replace(',', '');
if (this.cpuUsageEnabled) {
var cpuUsage = await AppApiVr.CpuUsage();
this.cpuUsage = cpuUsage.toFixed(0);
if ($app.cpuUsageEnabled) {
const cpuUsage = await AppApiVr.CpuUsage();
$app.cpuUsage = cpuUsage.toFixed(0);
}
if (this.lastLocation.date !== 0) {
this.lastLocationTimer = timeToText(
Date.now() - this.lastLocation.date
if ($app.lastLocation.date !== 0) {
$app.lastLocationTimer = timeToText(
Date.now() - $app.lastLocation.date
);
} else {
this.lastLocationTimer = '';
$app.lastLocationTimer = '';
}
if (this.lastLocation.onlineFor) {
this.onlineForTimer = timeToText(
Date.now() - this.lastLocation.onlineFor
if ($app.lastLocation.onlineFor) {
$app.onlineForTimer = timeToText(
Date.now() - $app.lastLocation.onlineFor
);
} else {
this.onlineForTimer = '';
$app.onlineForTimer = '';
}
if (!this.config.hideDevicesFromFeed) {
if (!$app.config.hideDevicesFromFeed) {
AppApiVr.GetVRDevices().then((devices) => {
var deviceList = [];
var baseStations = 0;
let deviceList = [];
let baseStations = 0;
devices.forEach((device) => {
device[3] = parseInt(device[3], 10);
if (device[0] === 'base' && device[1] === 'connected') {
@@ -338,7 +347,7 @@ Vue.component('marquee-text', MarqueeText);
deviceList.push(device);
}
});
this.deviceCount = deviceList.length;
$app.deviceCount = deviceList.length;
const deviceValue = (dev) => {
if (dev[0] === 'headset') return 0;
if (dev[0] === 'leftController') return 1;
@@ -368,38 +377,89 @@ Vue.component('marquee-text', MarqueeText);
'',
baseStations
]);
this.deviceCount += 1;
$app.deviceCount += 1;
}
this.devices = deviceList;
$app.devices = deviceList;
});
} else {
this.devices = [];
$app.devices = [];
}
if (this.config.pcUptimeOnFeed) {
if ($app.config.pcUptimeOnFeed) {
AppApiVr.GetUptime().then((uptime) => {
if (uptime) {
this.pcUptime = timeToText(uptime);
$app.pcUptime = timeToText(uptime);
}
});
} else {
this.pcUptime = '';
$app.pcUptime = '';
}
} catch (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) {
var { noty, message, image } = JSON.parse(json);
let { noty, message, image } = JSON.parse(json);
if (typeof noty === 'undefined') {
console.error('noty is undefined');
return;
}
var noty = escapeTagRecursive(noty);
var message = escapeTag(message) || '';
var text = '';
var img = '';
noty = escapeTagRecursive(noty);
message = escapeTag(message) || '';
let text = '';
let img = '';
if (image) {
img = `<img class="noty-img" src="${image}"></img>`;
}
@@ -423,7 +483,7 @@ Vue.component('marquee-text', MarqueeText);
)}`;
break;
case 'Online':
var locationName = '';
let locationName = '';
if (noty.worldName) {
locationName = ` to ${displayLocation(
noty.location,
@@ -444,7 +504,8 @@ Vue.component('marquee-text', MarqueeText);
noty.senderUsername
}</strong> has invited you to ${displayLocation(
noty.details.worldId,
noty.details.worldName
noty.details.worldName,
''
)}${message}`;
break;
case 'requestInvite':
@@ -556,16 +617,16 @@ Vue.component('marquee-text', MarqueeText);
if (text) {
new Noty({
type: 'alert',
theme: this.config.notificationTheme,
timeout: this.config.notificationTimeout,
layout: this.config.notificationPosition,
theme: $app.config.notificationTheme,
timeout: $app.config.notificationTimeout,
layout: $app.config.notificationPosition,
text: `${img}<div class="noty-text">${text}</div>`
}).show();
}
};
$app.methods.statusClass = function (status) {
var style = {};
let style = {};
if (typeof status === 'undefined') {
return style;
}
@@ -593,56 +654,56 @@ Vue.component('marquee-text', MarqueeText);
$app.data.cleanHudFeedLoopStatus = false;
$app.methods.cleanHudFeedLoop = function () {
if (!this.cleanHudFeedLoopStatus) {
if (!$app.cleanHudFeedLoopStatus) {
return;
}
this.cleanHudFeed();
if (this.hudFeed.length === 0) {
this.cleanHudFeedLoopStatus = false;
$app.cleanHudFeed();
if ($app.hudFeed.length === 0) {
$app.cleanHudFeedLoopStatus = false;
return;
}
workerTimers.setTimeout(() => this.cleanHudFeedLoop(), 500);
workerTimers.setTimeout(() => $app.cleanHudFeedLoop(), 500);
};
$app.methods.cleanHudFeed = function () {
var dt = Date.now();
this.hudFeed.forEach((item) => {
if (item.time + this.config.photonOverlayMessageTimeout < dt) {
removeFromArray(this.hudFeed, item);
const dt = Date.now();
$app.hudFeed.forEach((item) => {
if (item.time + $app.config.photonOverlayMessageTimeout < dt) {
removeFromArray($app.hudFeed, item);
}
});
if (this.hudFeed.length > 10) {
this.hudFeed.length = 10;
if ($app.hudFeed.length > 10) {
$app.hudFeed.length = 10;
}
if (!this.cleanHudFeedLoopStatus) {
this.cleanHudFeedLoopStatus = true;
this.cleanHudFeedLoop();
if (!$app.cleanHudFeedLoopStatus) {
$app.cleanHudFeedLoopStatus = true;
$app.cleanHudFeedLoop();
}
};
$app.methods.addEntryHudFeed = function (json) {
var data = JSON.parse(json);
var combo = 1;
this.hudFeed.forEach((item) => {
const data = JSON.parse(json);
let combo = 1;
$app.hudFeed.forEach((item) => {
if (
item.displayName === data.displayName &&
item.text === data.text
) {
combo = item.combo + 1;
removeFromArray(this.hudFeed, item);
removeFromArray($app.hudFeed, item);
}
});
this.hudFeed.unshift({
$app.hudFeed.unshift({
time: Date.now(),
combo,
...data
});
this.cleanHudFeed();
$app.cleanHudFeed();
};
$app.methods.updateHudFeedTag = function (json) {
var ref = JSON.parse(json);
this.hudFeed.forEach((item) => {
const ref = JSON.parse(json);
$app.hudFeed.forEach((item) => {
if (item.userId === ref.userId) {
item.colour = ref.colour;
}
@@ -652,16 +713,16 @@ Vue.component('marquee-text', MarqueeText);
$app.data.hudTimeout = [];
$app.methods.updateHudTimeout = function (json) {
this.hudTimeout = JSON.parse(json);
$app.hudTimeout = JSON.parse(json);
};
$app.methods.setDatetimeFormat = async function () {
this.currentCulture = await AppApiVr.CurrentCulture();
var formatDate = function (date) {
$app.currentCulture = await AppApiVr.CurrentCulture();
const formatDate = function (date) {
if (!date) {
return '';
}
var dt = new Date(date);
const dt = new Date(date);
return dt
.toLocaleTimeString($app.currentCulture, {
hour: '2-digit',
@@ -678,9 +739,9 @@ Vue.component('marquee-text', MarqueeText);
if (!appLanguage) {
return;
}
if (appLanguage !== this.appLanguage) {
this.appLanguage = appLanguage;
i18n.locale = this.appLanguage;
if (appLanguage !== $app.appLanguage) {
$app.appLanguage = appLanguage;
i18n.locale = $app.appLanguage;
}
};
@@ -688,8 +749,8 @@ Vue.component('marquee-text', MarqueeText);
if (document.contains(document.getElementById('app-emoji-font'))) {
document.getElementById('app-emoji-font').remove();
}
if (this.isRunningUnderWine) {
var $appEmojiFont = document.createElement('link');
if ($app.isRunningUnderWine) {
const $appEmojiFont = document.createElement('link');
$appEmojiFont.setAttribute('id', 'app-emoji-font');
$appEmojiFont.rel = 'stylesheet';
$appEmojiFont.href = 'emoji.font.css';

View File

@@ -8,7 +8,7 @@
// 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 '~noty/lib/noty.css';
@@ -20,6 +20,10 @@
손등 18px -> 나나 24
*/
body {
margin: 0;
}
.noty_body {
display: block;
}
@@ -171,13 +175,20 @@
border-radius: 16px;
}
@font-face {
font-family: 'ellipsis-font';
src: local('Times New Roman');
unicode-range: U+2026;
}
body,
input,
textarea,
select,
button {
font-family: 'Noto Sans JP', 'Noto Sans KR', 'Noto Sans TC', 'Noto Sans SC',
'Meiryo UI', 'Malgun Gothic', 'Segoe UI', sans-serif;
font-family:
'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;
text-shadow:
#000 0px 0px 3px,