Electron support for Linux (#1074)
* init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Fix UI var * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * init * SQLite changes * Move html folder, edit build scripts * AppApi interface * Build flags * AppApi inheritance * Finishing touches * Merge upstream changes * Test CI * Fix class inits * Rename AppApi * Merge upstream changes * Fix SQLiteLegacy on Linux, Add Linux interop, build tools * Linux specific localisation strings * Make it run * Bring back most of Linux functionality * Clean up * Fix TTS voices * Changes * Electron minimise to tray * Remove separate toggle for WlxOverlay * Fixes * Touchups * Move csproj * Window zoom, Desktop Notifications, VR check on Linux * Fix desktop notifications, VR check spam * Fix building on Linux * Clean up * Fix WebApi headers * Rewrite VRCX updater * Clean up * Linux updater * Add Linux to build action * Test updater * Rebase and handle merge conflicts * Fix Linux updater * Fix Linux app restart * Fix friend order * Handle AppImageInstaller, show an install message on Linux * Updates to the AppImage installer * Fix Linux updater, fix set version, check for .NET, copy wine prefix * Handle random errors * Rotate tall prints * try fix Linux restart bug * Final --------- Co-authored-by: rs189 <35667100+rs189@users.noreply.github.com>
@@ -32,7 +32,7 @@
|
||||
"AppApiVr": "readonly",
|
||||
"SharedVariable": "readonly",
|
||||
"WebApi": "readonly",
|
||||
"AssetBundleCacher": "readonly"
|
||||
"AssetBundleManager": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"arrow-body-style": 0,
|
||||
171
.github/workflows/github_actions.yml
vendored
@@ -3,45 +3,174 @@ name: VRCX
|
||||
on:
|
||||
- workflow_dispatch
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build_dotnet:
|
||||
runs-on: windows-latest
|
||||
set_version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: version
|
||||
run: |
|
||||
git_hash=$(git rev-parse --short "$GITHUB_SHA")
|
||||
export FILENAME="$(date '+%Y-%m-%dT%H.%M')-${git_hash}"
|
||||
echo "::set-output name=version::${FILENAME}"
|
||||
|
||||
build_dotnet_windows:
|
||||
runs-on: windows-latest
|
||||
needs: set_version
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET 8
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
- name: Build .NET Application
|
||||
run: dotnet build -p:Configuration=Release -p:Platform=x64 -p:EnableWindowsTargeting=true --self-contained
|
||||
- uses: actions/upload-artifact@v3
|
||||
- name: Set version
|
||||
run: |
|
||||
echo "${{ needs.set_version.outputs.version }}" > Version
|
||||
cat Version
|
||||
- name: Build Cef .NET Application
|
||||
run: dotnet build Dotnet\VRCX-Cef.csproj -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
|
||||
- name: Upload Cef dotnet artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: vrcx
|
||||
path: bin/x64/Release
|
||||
name: Cef-Release
|
||||
path: build/Cef
|
||||
|
||||
build_node:
|
||||
build_dotnet_linux:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: html
|
||||
needs: set_version
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup .NET 8
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
- name: Set version
|
||||
run: |
|
||||
echo "${{ needs.set_version.outputs.version }}" > Version
|
||||
cat Version
|
||||
- name: Build Electron .NET Application
|
||||
run: dotnet build 'Dotnet/VRCX-Electron.csproj' -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
|
||||
- name: Upload Electron dotnet artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Electron-Release
|
||||
path: build/Electron
|
||||
|
||||
build_node:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [set_version, build_dotnet_linux]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Set version
|
||||
run: |
|
||||
echo "${{ needs.set_version.outputs.version }}" > Version
|
||||
cat Version
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Restore dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run production
|
||||
- name: Fix folder structure
|
||||
id: fix-folders
|
||||
run: |
|
||||
mkdir upload
|
||||
mv dist upload/html
|
||||
- uses: actions/upload-artifact@v3
|
||||
- name: Build Cef-html
|
||||
run: npm run prod
|
||||
- name: Upload Cef-html artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: vrcx
|
||||
path: html/upload
|
||||
name: Cef-html
|
||||
path: build/html
|
||||
|
||||
- name: Build Electron-html
|
||||
run: npm run prod-linux
|
||||
- name: Download Electron dotnet artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Electron-Release
|
||||
path: build/Electron
|
||||
- name: Build AppImage
|
||||
run: npm run build-electron
|
||||
- name: Upload Electron AppImage artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Electron-AppImage
|
||||
path: "build/VRCX_${{ needs.set_version.outputs.version }}.AppImage"
|
||||
|
||||
create_setup:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [set_version, build_node, build_dotnet_windows, build_dotnet_linux]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install 7-zip and makensis
|
||||
run: sudo apt update && sudo apt install -y p7zip-full nsis nsis-pluginapi
|
||||
- name: Set plugin permissions
|
||||
run: sudo chown -R $(whoami) /usr/share/nsis/Plugins/
|
||||
- name: Download Cef dotnet artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Cef-Release
|
||||
path: build/Cef
|
||||
- name: Download Cef-html artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Cef-html
|
||||
path: build/Cef/html
|
||||
- name: Create nsis installer
|
||||
uses: joncloud/makensis-action@v4
|
||||
with:
|
||||
script-file: Installer/installer.nsi
|
||||
additional-plugin-paths: Installer/Plugins
|
||||
- name: Rename setup
|
||||
run: |
|
||||
file_name="VRCX_${{ needs.set_version.outputs.version }}_Setup.exe"
|
||||
echo "Setup FileName: ${file_name}"
|
||||
mv Installer/VRCX_Setup.exe $file_name
|
||||
- name: Make zip
|
||||
run: |
|
||||
file_name="VRCX_${{ needs.set_version.outputs.version }}.zip"
|
||||
cd build/Cef
|
||||
7z a -tzip ${file_name} * -mx=7 -xr0!*.log
|
||||
mv ${file_name} ../../${file_name}
|
||||
echo "Zip FileName: ${file_name}"
|
||||
|
||||
- name: Download Electron AppImage artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Electron-AppImage
|
||||
|
||||
- name: Generate hashes
|
||||
run: |
|
||||
sha256sum "VRCX_${{ needs.set_version.outputs.version }}_Setup.exe" "VRCX_${{ needs.set_version.outputs.version }}.AppImage" > "SHA256SUMS.txt"
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: "${{ needs.set_version.outputs.version }}"
|
||||
release_name: "VRCX Nightly ${{ needs.set_version.outputs.version }}"
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Upload multiple assets to release
|
||||
uses: csexton/release-asset-action@v2
|
||||
with:
|
||||
files: |
|
||||
VRCX_${{ needs.set_version.outputs.version }}_Setup.exe
|
||||
VRCX_${{ needs.set_version.outputs.version }}.zip
|
||||
VRCX_${{ needs.set_version.outputs.version }}.AppImage
|
||||
SHA256SUMS.txt
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
release-url: ${{ steps.create_release.outputs.upload_url }}
|
||||
|
||||
265
.gitignore
vendored
@@ -1,262 +1,7 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
#*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignoreable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
build/
|
||||
obj/
|
||||
obj1/
|
||||
.idea/
|
||||
*.sln.iml
|
||||
*.DotSettings
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.log
|
||||
*.DotSettings.user
|
||||
20
.vscode/settings.json
vendored
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": ["html/src/localization"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"omnisharp.useModernNet": false,
|
||||
"[csharp]": {
|
||||
"editor.defaultFormatter": "ms-dotnettools.csharp"
|
||||
}
|
||||
"i18n-ally.localesPaths": ["src/localization"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"omnisharp.useModernNet": false,
|
||||
"[csharp]": {
|
||||
"editor.defaultFormatter": "ms-dotnettools.csharp"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,727 +0,0 @@
|
||||
// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using librsync.net;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Microsoft.Win32;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public static readonly AppApi Instance;
|
||||
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly MD5 _hasher = MD5.Create();
|
||||
|
||||
static AppApi()
|
||||
{
|
||||
Instance = new AppApi();
|
||||
|
||||
ProcessMonitor.Instance.ProcessStarted += Instance.OnProcessStateChanged;
|
||||
ProcessMonitor.Instance.ProcessExited += Instance.OnProcessStateChanged;
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
// Create Instance before Cef tries to bind it
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the MD5 hash of the file represented by the specified base64-encoded string.
|
||||
/// </summary>
|
||||
/// <param name="Blob">The base64-encoded string representing the file.</param>
|
||||
/// <returns>The MD5 hash of the file as a base64-encoded string.</returns>
|
||||
public string MD5File(string Blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64CharArray(Blob.ToCharArray(), 0, Blob.Length);
|
||||
using (var md5 = MD5.Create())
|
||||
{
|
||||
var md5Hash = md5.ComputeHash(fileData);
|
||||
return Convert.ToBase64String(md5Hash);
|
||||
}
|
||||
}
|
||||
|
||||
public string ResizeImageToFitLimits(string base64data)
|
||||
{
|
||||
return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false));
|
||||
}
|
||||
|
||||
public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000)
|
||||
{
|
||||
using var fileMemoryStream = new MemoryStream(imageData);
|
||||
var image = new Bitmap(fileMemoryStream);
|
||||
|
||||
// for APNG, check if image is png format and less than maxSize
|
||||
if ((!matchingDimensions || image.Width == image.Height) &&
|
||||
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) &&
|
||||
imageData.Length < maxSize &&
|
||||
image.Width <= maxWidth &&
|
||||
image.Height <= maxHeight)
|
||||
{
|
||||
return imageData;
|
||||
}
|
||||
|
||||
if (image.Width > maxWidth)
|
||||
{
|
||||
var sizingFactor = image.Width / (double)maxWidth;
|
||||
var newHeight = (int)Math.Round(image.Height / sizingFactor);
|
||||
image = new Bitmap(image, maxWidth, newHeight);
|
||||
}
|
||||
if (image.Height > maxHeight)
|
||||
{
|
||||
var sizingFactor = image.Height / (double)maxHeight;
|
||||
var newWidth = (int)Math.Round(image.Width / sizingFactor);
|
||||
image = new Bitmap(image, newWidth, maxHeight);
|
||||
}
|
||||
if (matchingDimensions && image.Width != image.Height)
|
||||
{
|
||||
var newSize = Math.Max(image.Width, image.Height);
|
||||
var newImage = new Bitmap(newSize, newSize);
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
graphics.Clear(Color.Transparent);
|
||||
graphics.DrawImage(image, new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height));
|
||||
image.Dispose();
|
||||
image = newImage;
|
||||
}
|
||||
|
||||
SaveToFileToUpload();
|
||||
for (int i = 0; i < 250 && imageData.Length > maxSize; i++)
|
||||
{
|
||||
SaveToFileToUpload();
|
||||
if (imageData.Length < maxSize)
|
||||
break;
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
if (image.Width > image.Height)
|
||||
{
|
||||
newWidth = image.Width - 25;
|
||||
newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
newHeight = image.Height - 25;
|
||||
newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight));
|
||||
}
|
||||
image = new Bitmap(image, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (imageData.Length > maxSize)
|
||||
{
|
||||
throw new Exception("Failed to get image into target filesize.");
|
||||
}
|
||||
|
||||
return imageData;
|
||||
|
||||
void SaveToFileToUpload()
|
||||
{
|
||||
using var imageSaveMemoryStream = new MemoryStream();
|
||||
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
imageData = imageSaveMemoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] ResizePrintImage(byte[] imageData)
|
||||
{
|
||||
const int desiredWidth = 1920;
|
||||
const int desiredHeight = 1080;
|
||||
|
||||
using var fileMemoryStream = new MemoryStream(imageData);
|
||||
var image = new Bitmap(fileMemoryStream);
|
||||
|
||||
// increase size to 1920x1080
|
||||
if (image.Width < desiredWidth || image.Height < desiredHeight)
|
||||
{
|
||||
var newHeight = image.Height;
|
||||
var newWidth = image.Width;
|
||||
if (image.Width < desiredWidth)
|
||||
{
|
||||
var testHeight = (int)Math.Round(image.Height / (image.Width / (double)desiredWidth));
|
||||
if (testHeight <= desiredHeight)
|
||||
{
|
||||
newWidth = desiredWidth;
|
||||
newHeight = testHeight;
|
||||
}
|
||||
}
|
||||
if (image.Height < desiredHeight)
|
||||
{
|
||||
var testWidth = (int)Math.Round(image.Width / (image.Height / (double)desiredHeight));
|
||||
if (testWidth <= desiredWidth)
|
||||
{
|
||||
newHeight = desiredHeight;
|
||||
newWidth = testWidth;
|
||||
}
|
||||
}
|
||||
var resizedImage = new Bitmap(desiredWidth, desiredHeight);
|
||||
using var graphics1 = Graphics.FromImage(resizedImage);
|
||||
graphics1.Clear(Color.White);
|
||||
var x = (desiredWidth - newWidth) / 2;
|
||||
var y = (desiredHeight - newHeight) / 2;
|
||||
graphics1.DrawImage(image, new Rectangle(x, y, newWidth, newHeight));
|
||||
image.Dispose();
|
||||
image = resizedImage;
|
||||
}
|
||||
|
||||
// limit size to 1920x1080
|
||||
if (image.Width > desiredWidth)
|
||||
{
|
||||
var sizingFactor = image.Width / (double)desiredWidth;
|
||||
var newHeight = (int)Math.Round(image.Height / sizingFactor);
|
||||
image = new Bitmap(image, desiredWidth, newHeight);
|
||||
}
|
||||
if (image.Height > desiredHeight)
|
||||
{
|
||||
var sizingFactor = image.Height / (double)desiredHeight;
|
||||
var newWidth = (int)Math.Round(image.Width / sizingFactor);
|
||||
image = new Bitmap(image, newWidth, desiredHeight);
|
||||
}
|
||||
|
||||
// add white border
|
||||
// wtf are these magic numbers
|
||||
const int xOffset = 64; // 2048 / 32
|
||||
const int yOffset = 69; // 1440 / 20.869
|
||||
var newImage = new Bitmap(2048, 1440);
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
graphics.Clear(Color.White);
|
||||
// graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
|
||||
var newX = (2048 - image.Width) / 2;
|
||||
var newY = yOffset;
|
||||
graphics.DrawImage(image, new Rectangle(newX, newY, image.Width, image.Height));
|
||||
image.Dispose();
|
||||
image = newImage;
|
||||
|
||||
using var imageSaveMemoryStream = new MemoryStream();
|
||||
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
return imageSaveMemoryStream.ToArray();
|
||||
}
|
||||
|
||||
public async Task CropAllPrints(string ugcFolderPath)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints");
|
||||
var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
await CropPrintImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CropPrintImage(string path)
|
||||
{
|
||||
var tempPath = path + ".temp";
|
||||
var bytes = await File.ReadAllBytesAsync(path);
|
||||
var ms = new MemoryStream(bytes);
|
||||
Bitmap print = new Bitmap(ms);
|
||||
// validation step to ensure image is actually a print
|
||||
if (print.Width != 2048 || print.Height != 1440)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var point = new Point(64, 69);
|
||||
var size = new Size(1920, 1080);
|
||||
var rectangle = new Rectangle(point, size);
|
||||
Bitmap cropped = print.Clone(rectangle, print.PixelFormat);
|
||||
cropped.Save(tempPath);
|
||||
if (ScreenshotHelper.HasTXt(path))
|
||||
{
|
||||
var success = ScreenshotHelper.CopyTXt(path, tempPath);
|
||||
if (!success)
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File.Move(tempPath, path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the signature of the file represented by the specified base64-encoded string using the librsync library.
|
||||
/// </summary>
|
||||
/// <param name="Blob">The base64-encoded string representing the file.</param>
|
||||
/// <returns>The signature of the file as a base64-encoded string.</returns>
|
||||
public string SignFile(string Blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64String(Blob);
|
||||
using (var sig = Librsync.ComputeSignature(new MemoryStream(fileData)))
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
sig.CopyTo(memoryStream);
|
||||
var sigBytes = memoryStream.ToArray();
|
||||
return Convert.ToBase64String(sigBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the length of the file represented by the specified base64-encoded string.
|
||||
/// </summary>
|
||||
/// <param name="Blob">The base64-encoded string representing the file.</param>
|
||||
/// <returns>The length of the file in bytes.</returns>
|
||||
public string FileLength(string Blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64String(Blob);
|
||||
return fileData.Length.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the developer tools for the main browser window.
|
||||
/// </summary>
|
||||
public void ShowDevTools()
|
||||
{
|
||||
MainForm.Instance.Browser.ShowDevTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all cookies from the global cef cookie manager.
|
||||
/// </summary>
|
||||
public void DeleteAllCookies()
|
||||
{
|
||||
Cef.GetGlobalCookieManager().DeleteCookies();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the specified URL in the default browser.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL to open.</param>
|
||||
public void OpenLink(string url)
|
||||
{
|
||||
if (url.StartsWith("http://") ||
|
||||
url.StartsWith("https://"))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(url)
|
||||
{
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// broken since adding ExecuteVrFeedFunction(
|
||||
// public void ShowVRForm()
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
|
||||
// {
|
||||
// if (VRForm.Instance == null)
|
||||
// {
|
||||
// new VRForm().Show();
|
||||
// }
|
||||
// }));
|
||||
// }
|
||||
// catch
|
||||
// {
|
||||
// }
|
||||
// }
|
||||
|
||||
public void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
Program.VRCXVRInstance.SetActive(active, hmdOverlay, wristOverlay, menuButton, overlayHand);
|
||||
}
|
||||
|
||||
public void RefreshVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public void RestartVR()
|
||||
{
|
||||
Program.VRCXVRInstance.Restart();
|
||||
}
|
||||
|
||||
public void SetZoom(double zoomLevel)
|
||||
{
|
||||
MainForm.Instance.Browser.SetZoomLevel(zoomLevel);
|
||||
}
|
||||
|
||||
public async Task<double> GetZoom()
|
||||
{
|
||||
return await MainForm.Instance.Browser.GetZoomLevelAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an image from the VRChat API and caches it for future use. The function will return the cached image if it already exists.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL of the image to retrieve.</param>
|
||||
/// <param name="fileId">The ID of the file associated with the image.</param>
|
||||
/// <param name="version">The version of the file associated with the image.</param>
|
||||
/// <returns>A string representing the file location of the cached image.</returns>
|
||||
public async Task<string> GetImage(string url, string fileId, string version)
|
||||
{
|
||||
return await ImageCache.GetImage(url, fileId, version);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Displays a desktop notification with the specified bold text, optional text, and optional image.
|
||||
/// </summary>
|
||||
/// <param name="BoldText">The bold text to display in the notification.</param>
|
||||
/// <param name="Text">The optional text to display in the notification.</param>
|
||||
/// <param name="Image">The optional image to display in the notification.</param>
|
||||
public void DesktopNotification(string BoldText, string Text = "", string Image = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
ToastContentBuilder builder = new ToastContentBuilder();
|
||||
|
||||
if (Uri.TryCreate(Image, UriKind.Absolute, out Uri uri))
|
||||
builder.AddAppLogoOverride(uri);
|
||||
|
||||
if (!string.IsNullOrEmpty(BoldText))
|
||||
builder.AddText(BoldText);
|
||||
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
builder.AddText(Text);
|
||||
|
||||
builder.Show();
|
||||
}
|
||||
catch (System.AccessViolationException ex)
|
||||
{
|
||||
logger.Warn(ex, "Unable to send desktop notification");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Unknown error when sending desktop notification");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the VRCX application for an update by launching a new process with the upgrade argument and exiting the current process.
|
||||
/// </summary>
|
||||
public void RestartApplication(bool isUpgrade)
|
||||
{
|
||||
var args = new List<string>();
|
||||
|
||||
if (isUpgrade)
|
||||
args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix);
|
||||
|
||||
if (StartupArgs.LaunchArguments.IsDebug)
|
||||
args.Add(StartupArgs.VrcxLaunchArguments.IsDebugPrefix);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ConfigDirectory))
|
||||
args.Add($"{StartupArgs.VrcxLaunchArguments.ConfigDirectoryPrefix}={StartupArgs.LaunchArguments.ConfigDirectory}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ProxyUrl))
|
||||
args.Add($"{StartupArgs.VrcxLaunchArguments.ProxyUrlPrefix}={StartupArgs.LaunchArguments.ProxyUrl}");
|
||||
|
||||
var vrcxProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"),
|
||||
Arguments = string.Join(' ', args),
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Program.BaseDirectory
|
||||
}
|
||||
};
|
||||
vrcxProcess.Start();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the VRCX update executable exists in the AppData directory.
|
||||
/// </summary>
|
||||
/// <returns>True if the update executable exists, false otherwise.</returns>
|
||||
public bool CheckForUpdateExe()
|
||||
{
|
||||
if (File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe")))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an IPC packet to announce the start of VRCX.
|
||||
/// </summary>
|
||||
public void IPCAnnounceStart()
|
||||
{
|
||||
IPCServer.Send(new IPCPacket
|
||||
{
|
||||
Type = "VRCXLaunch",
|
||||
MsgType = "VRCXLaunch"
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an IPC packet with a specified message type and data.
|
||||
/// </summary>
|
||||
/// <param name="type">The message type to send.</param>
|
||||
/// <param name="data">The data to send.</param>
|
||||
public void SendIpc(string type, string data)
|
||||
{
|
||||
IPCServer.Send(new IPCPacket
|
||||
{
|
||||
Type = "VrcxMessage",
|
||||
MsgType = type,
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
|
||||
public void ExecuteAppFunction(string function, string json)
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", json);
|
||||
}
|
||||
|
||||
public void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrFeedFunction(function, json);
|
||||
}
|
||||
|
||||
public void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
Program.VRCXVRInstance.ExecuteVrOverlayFunction(function, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the launch command from the startup arguments and clears the launch command.
|
||||
/// </summary>
|
||||
/// <returns>The launch command.</returns>
|
||||
public string GetLaunchCommand()
|
||||
{
|
||||
var command = StartupArgs.LaunchArguments.LaunchCommand;
|
||||
StartupArgs.LaunchArguments.LaunchCommand = string.Empty;
|
||||
return command;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Focuses the main window of the VRCX application.
|
||||
/// </summary>
|
||||
public void FocusWindow()
|
||||
{
|
||||
MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user CSS file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user CSS file, or an empty string if it doesn't exist.</returns>
|
||||
public string CustomCssPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.css");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the file path of the custom user js file, if it exists.
|
||||
/// </summary>
|
||||
/// <returns>The file path of the custom user js file, or an empty string if it doesn't exist.</returns>
|
||||
public string CustomScriptPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
public string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
public string CurrentLanguage()
|
||||
{
|
||||
return CultureInfo.InstalledUICulture.Name;
|
||||
}
|
||||
|
||||
public string GetVersion()
|
||||
{
|
||||
return Program.Version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the VRChat client was last closed gracefully. According to the log file, anyway.
|
||||
/// </summary>
|
||||
/// <returns>True if the VRChat client was last closed gracefully, false otherwise.</returns>
|
||||
public bool VrcClosedGracefully()
|
||||
{
|
||||
return LogWatcher.Instance.VrcClosedGracefully;
|
||||
}
|
||||
|
||||
public void ChangeTheme(int value)
|
||||
{
|
||||
WinformThemer.SetGlobalTheme(value);
|
||||
}
|
||||
|
||||
public void DoFunny()
|
||||
{
|
||||
WinformThemer.DoFunny();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a color value derived from the given user ID.
|
||||
/// This is, essentially, and is used for, random colors.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user ID to derive the color value from.</param>
|
||||
/// <returns>A color value derived from the given user ID.</returns>
|
||||
public int GetColourFromUserID(string userId)
|
||||
{
|
||||
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
|
||||
return (hash[3] << 8) | hash[4];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a dictionary of color values derived from the given list of user IDs.
|
||||
/// </summary>
|
||||
/// <param name="userIds">The list of user IDs to derive the color values from.</param>
|
||||
/// <returns>A dictionary of color values derived from the given list of user IDs.</returns>
|
||||
public Dictionary<string, int> GetColourBulk(List<object> userIds)
|
||||
{
|
||||
var output = new Dictionary<string, int>();
|
||||
foreach (string userId in userIds)
|
||||
{
|
||||
output.Add(userId, GetColourFromUserID(userId));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the current text from the clipboard.
|
||||
/// </summary>
|
||||
/// <returns>The current text from the clipboard.</returns>
|
||||
public string GetClipboard()
|
||||
{
|
||||
var clipboard = string.Empty;
|
||||
var thread = new Thread(() => clipboard = Clipboard.GetText());
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
return clipboard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether or not the application should start up automatically with Windows.
|
||||
/// </summary>
|
||||
/// <param name="enabled">True to enable automatic startup, false to disable it.</param>
|
||||
public void SetStartup(bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true))
|
||||
{
|
||||
if (enabled)
|
||||
{
|
||||
var path = System.Windows.Forms.Application.ExecutablePath;
|
||||
key.SetValue("VRCX", $"\"{path}\" --startup");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue("VRCX", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// what the fuck even is this
|
||||
// refactor when
|
||||
// #AppApiLivesDontMatter
|
||||
public void SetAppLauncherSettings(bool enabled, bool killOnExit)
|
||||
{
|
||||
AutoAppLaunchManager.Instance.Enabled = enabled;
|
||||
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies an image file to the clipboard if it exists and is of a supported image file type.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the image file to copy to the clipboard.</param>
|
||||
public void CopyImageToClipboard(string path)
|
||||
{
|
||||
// check if the file exists and is any image file type
|
||||
if (File.Exists(path) && (path.EndsWith(".png") || path.EndsWith(".jpg") || path.EndsWith(".jpeg") || path.EndsWith(".gif") || path.EndsWith(".bmp") || path.EndsWith(".webp")))
|
||||
{
|
||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
|
||||
{
|
||||
var image = System.Drawing.Image.FromFile(path);
|
||||
// Clipboard.SetImage(image);
|
||||
var data = new DataObject();
|
||||
data.SetData(DataFormats.Bitmap, image);
|
||||
data.SetFileDropList(new StringCollection { path });
|
||||
Clipboard.SetDataObject(data, true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flashes the window of the main form.
|
||||
/// </summary>
|
||||
public void FlashWindow()
|
||||
{
|
||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); }));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user agent string for the browser.
|
||||
/// </summary>
|
||||
public void SetUserAgent()
|
||||
{
|
||||
using (var client = MainForm.Instance.Browser.GetDevToolsClient())
|
||||
{
|
||||
_ = client.Network.SetUserAgentOverrideAsync(Program.Version);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetFileBase64(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return Convert.ToBase64String(File.ReadAllBytes(path));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder));
|
||||
Directory.CreateDirectory(folder);
|
||||
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
|
||||
if (File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var success = await ImageCache.SaveImageToFile(url, filePath);
|
||||
|
||||
return success ? filePath : null;
|
||||
}
|
||||
|
||||
public async Task<string> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder));
|
||||
Directory.CreateDirectory(folder);
|
||||
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
|
||||
if (File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var success = await ImageCache.SaveImageToFile(url, filePath);
|
||||
|
||||
return success ? filePath : null;
|
||||
}
|
||||
|
||||
public bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
}
|
||||
}
|
||||
}
|
||||
247
Dotnet/AppApi/Cef/AppApiCef.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright(c) 2019-2022 pypy, Natsumi and individual contributors.
|
||||
// All rights reserved.
|
||||
//
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using librsync.net;
|
||||
using Microsoft.Toolkit.Uwp.Notifications;
|
||||
using Microsoft.Win32;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef : AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Shows the developer tools for the main browser window.
|
||||
/// </summary>
|
||||
public override void ShowDevTools()
|
||||
{
|
||||
MainForm.Instance.Browser.ShowDevTools();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all cookies from the global cef cookie manager.
|
||||
/// </summary>
|
||||
public override void DeleteAllCookies()
|
||||
{
|
||||
Cef.GetGlobalCookieManager().DeleteCookies();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
MainForm.Instance.Browser.SetZoomLevel(zoomLevel);
|
||||
}
|
||||
|
||||
public override async Task<double> GetZoom()
|
||||
{
|
||||
return await MainForm.Instance.Browser.GetZoomLevelAsync();
|
||||
}
|
||||
|
||||
public override void DesktopNotification(string BoldText, string Text = "", string Image = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
ToastContentBuilder builder = new ToastContentBuilder();
|
||||
|
||||
if (Uri.TryCreate(Image, UriKind.Absolute, out Uri uri))
|
||||
builder.AddAppLogoOverride(uri);
|
||||
|
||||
if (!string.IsNullOrEmpty(BoldText))
|
||||
builder.AddText(BoldText);
|
||||
|
||||
if (!string.IsNullOrEmpty(Text))
|
||||
builder.AddText(Text);
|
||||
|
||||
builder.Show();
|
||||
}
|
||||
catch (System.AccessViolationException ex)
|
||||
{
|
||||
logger.Warn(ex, "Unable to send desktop notification");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Unknown error when sending desktop notification");
|
||||
}
|
||||
}
|
||||
|
||||
public override void RestartApplication(bool isUpgrade)
|
||||
{
|
||||
var args = new List<string>();
|
||||
|
||||
if (isUpgrade)
|
||||
args.Add(StartupArgs.VrcxLaunchArguments.IsUpgradePrefix);
|
||||
|
||||
if (StartupArgs.LaunchArguments.IsDebug)
|
||||
args.Add(StartupArgs.VrcxLaunchArguments.IsDebugPrefix);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ConfigDirectory))
|
||||
args.Add($"{StartupArgs.VrcxLaunchArguments.ConfigDirectoryPrefix}={StartupArgs.LaunchArguments.ConfigDirectory}");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(StartupArgs.LaunchArguments.ProxyUrl))
|
||||
args.Add($"{StartupArgs.VrcxLaunchArguments.ProxyUrlPrefix}={StartupArgs.LaunchArguments.ProxyUrl}");
|
||||
|
||||
var vrcxProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = Path.Combine(Program.BaseDirectory, "VRCX.exe"),
|
||||
Arguments = string.Join(' ', args),
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Program.BaseDirectory
|
||||
}
|
||||
};
|
||||
vrcxProcess.Start();
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
public override bool CheckForUpdateExe()
|
||||
{
|
||||
return File.Exists(Path.Combine(Program.AppDataDirectory, "update.exe"));
|
||||
}
|
||||
|
||||
public override void ExecuteAppFunction(string function, string json)
|
||||
{
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync($"$app.{function}", 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 string GetLaunchCommand()
|
||||
{
|
||||
var command = StartupArgs.LaunchArguments.LaunchCommand;
|
||||
StartupArgs.LaunchArguments.LaunchCommand = string.Empty;
|
||||
return command;
|
||||
}
|
||||
|
||||
public override void FocusWindow()
|
||||
{
|
||||
MainForm.Instance.Invoke(new Action(() => { MainForm.Instance.Focus_Window(); }));
|
||||
}
|
||||
|
||||
public override void ChangeTheme(int value)
|
||||
{
|
||||
WinformThemer.SetGlobalTheme(value);
|
||||
}
|
||||
|
||||
public override void DoFunny()
|
||||
{
|
||||
WinformThemer.DoFunny();
|
||||
}
|
||||
|
||||
public override string GetClipboard()
|
||||
{
|
||||
var clipboard = string.Empty;
|
||||
var thread = new Thread(() => clipboard = Clipboard.GetText());
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
return clipboard;
|
||||
}
|
||||
|
||||
public override void SetStartup(bool enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
|
||||
if (key == null)
|
||||
{
|
||||
logger.Warn("Failed to open startup registry key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
var path = Application.ExecutablePath;
|
||||
key.SetValue("VRCX", $"\"{path}\" --startup");
|
||||
}
|
||||
else
|
||||
{
|
||||
key.DeleteValue("VRCX", false);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Warn(e, "Failed to set startup");
|
||||
}
|
||||
}
|
||||
|
||||
public override void CopyImageToClipboard(string path)
|
||||
{
|
||||
if (!File.Exists(path) ||
|
||||
(!path.EndsWith(".png") &&
|
||||
!path.EndsWith(".jpg") &&
|
||||
!path.EndsWith(".jpeg") &&
|
||||
!path.EndsWith(".gif") &&
|
||||
!path.EndsWith(".bmp") &&
|
||||
!path.EndsWith(".webp")))
|
||||
return;
|
||||
|
||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() =>
|
||||
{
|
||||
var image = Image.FromFile(path);
|
||||
// Clipboard.SetImage(image);
|
||||
var data = new DataObject();
|
||||
data.SetData(DataFormats.Bitmap, image);
|
||||
data.SetFileDropList(new StringCollection { path });
|
||||
Clipboard.SetDataObject(data, true);
|
||||
}));
|
||||
}
|
||||
|
||||
public override void FlashWindow()
|
||||
{
|
||||
MainForm.Instance.BeginInvoke(new MethodInvoker(() => { WinformThemer.Flash(MainForm.Instance); }));
|
||||
}
|
||||
|
||||
public override void SetUserAgent()
|
||||
{
|
||||
using var client = MainForm.Instance.Browser.GetDevToolsClient();
|
||||
_ = client.Network.SetUserAgentOverrideAsync(Program.Version);
|
||||
}
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return Wine.GetIfWine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,14 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
public partial class AppApiCef
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the VRChat application data location by reading the config file and checking the cache directory.
|
||||
/// If the cache directory is not found in the config file, it returns the default cache path.
|
||||
/// </summary>
|
||||
/// <returns>The VRChat application data location.</returns>
|
||||
public string GetVRChatAppDataLocation()
|
||||
public override string GetVRChatAppDataLocation()
|
||||
{
|
||||
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
|
||||
}
|
||||
|
||||
public override string GetVRChatCacheLocation()
|
||||
{
|
||||
var json = ReadConfigFile();
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
@@ -35,10 +35,10 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
|
||||
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
|
||||
}
|
||||
|
||||
public string GetVRChatPhotosLocation()
|
||||
public override string GetVRChatPhotosLocation()
|
||||
{
|
||||
var json = ReadConfigFile();
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
@@ -56,13 +56,8 @@ namespace VRCX
|
||||
|
||||
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "VRChat");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder the user has selected for User-Generated content such as prints / stickers from the JS side.
|
||||
/// If there is no override on the folder, it returns the default VRChat Photos path.
|
||||
/// </summary>
|
||||
/// <returns>The UGC Photo Location.</returns>
|
||||
public string GetUGCPhotoLocation(string path = "")
|
||||
|
||||
public override string GetUGCPhotoLocation(string path = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
@@ -102,14 +97,15 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error($"Failed to get Steam userdata path from registry: {e}");
|
||||
}
|
||||
|
||||
return steamUserdataPath;
|
||||
}
|
||||
|
||||
public string GetVRChatScreenshotsLocation()
|
||||
public override string GetVRChatScreenshotsLocation()
|
||||
{
|
||||
// program files steam userdata screenshots
|
||||
var steamUserdataPath = GetSteamUserdataPathFromRegistry();
|
||||
@@ -136,16 +132,7 @@ namespace VRCX
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the VRChat cache location by combining the VRChat application data location with the cache directory name.
|
||||
/// </summary>
|
||||
/// <returns>The VRChat cache location.</returns>
|
||||
public string GetVRChatCacheLocation()
|
||||
{
|
||||
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
|
||||
}
|
||||
|
||||
public bool OpenVrcxAppDataFolder()
|
||||
public override bool OpenVrcxAppDataFolder()
|
||||
{
|
||||
var path = Program.AppDataDirectory;
|
||||
if (!Directory.Exists(path))
|
||||
@@ -155,7 +142,7 @@ namespace VRCX
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OpenVrcAppDataFolder()
|
||||
public override bool OpenVrcAppDataFolder()
|
||||
{
|
||||
var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
|
||||
if (!Directory.Exists(path))
|
||||
@@ -165,7 +152,7 @@ namespace VRCX
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OpenVrcPhotosFolder()
|
||||
public override bool OpenVrcPhotosFolder()
|
||||
{
|
||||
var path = GetVRChatPhotosLocation();
|
||||
if (!Directory.Exists(path))
|
||||
@@ -175,7 +162,7 @@ namespace VRCX
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OpenUGCPhotosFolder(string ugcPath = "")
|
||||
public override bool OpenUGCPhotosFolder(string ugcPath = "")
|
||||
{
|
||||
var path = GetUGCPhotoLocation(ugcPath);
|
||||
if (!Directory.Exists(path))
|
||||
@@ -185,7 +172,7 @@ namespace VRCX
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OpenVrcScreenshotsFolder()
|
||||
public override bool OpenVrcScreenshotsFolder()
|
||||
{
|
||||
var path = GetVRChatScreenshotsLocation();
|
||||
if (!Directory.Exists(path))
|
||||
@@ -195,7 +182,7 @@ namespace VRCX
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool OpenCrashVrcCrashDumps()
|
||||
public override bool OpenCrashVrcCrashDumps()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), "VRChat", "VRChat", "Crashes");
|
||||
if (!Directory.Exists(path))
|
||||
@@ -204,11 +191,8 @@ namespace VRCX
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the folder containing user-defined shortcuts, if it exists.
|
||||
/// </summary>
|
||||
public void OpenShortcutFolder()
|
||||
|
||||
public override void OpenShortcutFolder()
|
||||
{
|
||||
var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
|
||||
if (!Directory.Exists(path))
|
||||
@@ -216,13 +200,8 @@ namespace VRCX
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the folder containing the specified file or folder path and selects the item in the folder.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the file or folder to select in the folder.</param>
|
||||
/// <param name="isFolder">Whether the specified path is a folder or not. Defaults to false.</param>
|
||||
public void OpenFolderAndSelectItem(string path, bool isFolder = false)
|
||||
|
||||
public override void OpenFolderAndSelectItem(string path, bool isFolder = false)
|
||||
{
|
||||
path = Path.GetFullPath(path);
|
||||
// I don't think it's quite meant for it, but SHOpenFolderAndSelectItems can open folders by passing the folder path as the item to select, as a child to itself, somehow. So we'll check to see if 'path' is a folder as well.
|
||||
@@ -272,7 +251,7 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenFolderAndSelectItemFallback(string path)
|
||||
private void OpenFolderAndSelectItemFallback(string path)
|
||||
{
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
return;
|
||||
@@ -288,30 +267,24 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a folder dialog to select a folder and pass it back to the JS side.
|
||||
/// </summary>
|
||||
/// <param name="defaultPath">The default path for the folder picker.</param>
|
||||
public async Task<string> OpenFolderSelectorDialog(string defaultPath = "")
|
||||
public override async Task<string> OpenFolderSelectorDialog(string defaultPath = "")
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
var staThread = new Thread(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var openFolderDialog = new FolderBrowserDialog())
|
||||
{
|
||||
openFolderDialog.InitialDirectory = Directory.Exists(defaultPath) ? defaultPath : GetVRChatPhotosLocation();
|
||||
using var openFolderDialog = new FolderBrowserDialog();
|
||||
openFolderDialog.InitialDirectory = Directory.Exists(defaultPath) ? defaultPath : GetVRChatPhotosLocation();
|
||||
|
||||
var dialogResult = openFolderDialog.ShowDialog(MainForm.nativeWindow);
|
||||
if (dialogResult == DialogResult.OK)
|
||||
{
|
||||
tcs.SetResult(openFolderDialog.SelectedPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(defaultPath);
|
||||
}
|
||||
var dialogResult = openFolderDialog.ShowDialog(MainForm.nativeWindow);
|
||||
if (dialogResult == DialogResult.OK)
|
||||
{
|
||||
tcs.SetResult(openFolderDialog.SelectedPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
tcs.SetResult(defaultPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -325,12 +298,8 @@ namespace VRCX
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens a folder dialog to select a file and pass the path back to the JS side.
|
||||
/// </summary>
|
||||
/// <param name="defaultPath">The default path for the file picker.</param>
|
||||
public async Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "", string defaultFilter = "All files (*.*)|*.*")
|
||||
|
||||
public override async Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "", string defaultFilter = "All files (*.*)|*.*")
|
||||
{
|
||||
var tcs = new TaskCompletionSource<string>();
|
||||
var staThread = new Thread(() =>
|
||||
@@ -369,21 +338,5 @@ namespace VRCX
|
||||
|
||||
return await tcs.Task;
|
||||
}
|
||||
|
||||
private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
|
||||
Regex.Escape(new string(Path.GetInvalidPathChars()))));
|
||||
|
||||
private static readonly Regex _fileRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
|
||||
Regex.Escape(new string(Path.GetInvalidFileNameChars()))));
|
||||
|
||||
public static string MakeValidFileName(string name)
|
||||
{
|
||||
name = name.Replace("/", "");
|
||||
name = name.Replace("\\", "");
|
||||
name = _folderRegex.Replace(name, "");
|
||||
name = _fileRegex.Replace(name, "");
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
140
Dotnet/AppApi/Cef/GameHandler.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CefSharp;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef
|
||||
{
|
||||
public override void OnProcessStateChanged(MonitoredProcess monitoredProcess)
|
||||
{
|
||||
if (!monitoredProcess.HasName("VRChat") && !monitoredProcess.HasName("vrserver"))
|
||||
return;
|
||||
|
||||
CheckGameRunning();
|
||||
}
|
||||
|
||||
public override void CheckGameRunning()
|
||||
{
|
||||
var isGameRunning = false;
|
||||
var isSteamVRRunning = false;
|
||||
var isHmdAfk = false;
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("VRChat"))
|
||||
isGameRunning = true;
|
||||
|
||||
if (Wine.GetIfWine())
|
||||
{
|
||||
var wineTmpPath = Path.Combine(Program.AppDataDirectory, "wine.tmp");
|
||||
if (File.Exists(wineTmpPath))
|
||||
{
|
||||
var wineTmp = File.ReadAllText(wineTmpPath);
|
||||
if (wineTmp.Contains("isGameRunning=true"))
|
||||
isGameRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("vrserver"))
|
||||
isSteamVRRunning = true;
|
||||
|
||||
if (Program.VRCXVRInstance != null)
|
||||
isHmdAfk = Program.VRCXVRInstance.IsHmdAfk;
|
||||
|
||||
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
|
||||
}
|
||||
|
||||
public override bool IsGameRunning()
|
||||
{
|
||||
// unused
|
||||
return ProcessMonitor.Instance.IsProcessRunning("VRChat");
|
||||
}
|
||||
|
||||
public override bool IsSteamVRRunning()
|
||||
{
|
||||
// unused
|
||||
return ProcessMonitor.Instance.IsProcessRunning("vrserver");
|
||||
}
|
||||
|
||||
public override int QuitGame()
|
||||
{
|
||||
var processes = Process.GetProcessesByName("vrchat");
|
||||
if (processes.Length == 1)
|
||||
processes[0].Kill();
|
||||
|
||||
return processes.Length;
|
||||
}
|
||||
|
||||
public override bool StartGame(string arguments)
|
||||
{
|
||||
// try stream first
|
||||
try
|
||||
{
|
||||
using var key = Registry.ClassesRoot.OpenSubKey(@"steam\shell\open\command");
|
||||
// "C:\Program Files (x86)\Steam\steam.exe" -- "%1"
|
||||
var match = Regex.Match(key.GetValue(string.Empty) as string, "^\"(.+?)\\\\steam.exe\"");
|
||||
if (match.Success)
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
// var _arguments = Uri.EscapeDataString(arguments);
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = path,
|
||||
FileName = $"{path}\\steam.exe",
|
||||
UseShellExecute = false,
|
||||
Arguments = $"-applaunch 438100 {arguments}"
|
||||
})
|
||||
?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Warn("Failed to start VRChat from Steam");
|
||||
}
|
||||
|
||||
// fallback
|
||||
try
|
||||
{
|
||||
using var key = Registry.ClassesRoot.OpenSubKey(@"VRChat\shell\open\command");
|
||||
// "C:\Program Files (x86)\Steam\steamapps\common\VRChat\launch.exe" "%1" %*
|
||||
var match = Regex.Match(key.GetValue(string.Empty) as string, "(?!\")(.+?\\\\VRChat.*)(!?\\\\launch.exe\")");
|
||||
if (match.Success)
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
return StartGameFromPath(path, arguments);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Warn("Failed to start VRChat from registry");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool StartGameFromPath(string path, string arguments)
|
||||
{
|
||||
if (!path.EndsWith(".exe"))
|
||||
path = Path.Combine(path, "launch.exe");
|
||||
|
||||
if (!path.EndsWith("launch.exe") || !File.Exists(path))
|
||||
return false;
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(path),
|
||||
FileName = path,
|
||||
UseShellExecute = false,
|
||||
Arguments = arguments
|
||||
})?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,10 @@ using Microsoft.Win32;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
public partial class AppApiCef
|
||||
{
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
public static extern uint RegSetValueEx(
|
||||
private static extern uint RegSetValueEx(
|
||||
UIntPtr hKey,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string lpValueName,
|
||||
int Reserved,
|
||||
@@ -23,7 +23,7 @@ namespace VRCX
|
||||
int cbData);
|
||||
|
||||
[DllImport("advapi32.dll", CharSet = CharSet.Ansi, SetLastError = true)]
|
||||
public static extern int RegOpenKeyEx(
|
||||
private static extern int RegOpenKeyEx(
|
||||
UIntPtr hKey,
|
||||
string subKey,
|
||||
int ulOptions,
|
||||
@@ -31,9 +31,9 @@ namespace VRCX
|
||||
out UIntPtr hkResult);
|
||||
|
||||
[DllImport("advapi32.dll")]
|
||||
public static extern int RegCloseKey(UIntPtr hKey);
|
||||
private static extern int RegCloseKey(UIntPtr hKey);
|
||||
|
||||
public string AddHashToKeyName(string key)
|
||||
private string AddHashToKeyName(string key)
|
||||
{
|
||||
// https://discussions.unity.com/t/playerprefs-changing-the-name-of-keys/30332/4
|
||||
// VRC_GROUP_ORDER_usr_032383a7-748c-4fb2-94e4-bcb928e5de6b_h2810492971
|
||||
@@ -48,35 +48,39 @@ namespace VRCX
|
||||
/// </summary>
|
||||
/// <param name="key">The name of the key to retrieve.</param>
|
||||
/// <returns>The value of the specified key, or null if the key does not exist.</returns>
|
||||
public object GetVRChatRegistryKey(string key)
|
||||
public override object GetVRChatRegistryKey(string key)
|
||||
{
|
||||
var keyName = AddHashToKeyName(key);
|
||||
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
var data = regKey?.GetValue(keyName);
|
||||
if (data == null)
|
||||
return null;
|
||||
|
||||
var type = regKey.GetValueKind(keyName);
|
||||
switch (type)
|
||||
{
|
||||
var data = regKey?.GetValue(keyName);
|
||||
if (data == null)
|
||||
return null;
|
||||
case RegistryValueKind.Binary:
|
||||
return Encoding.ASCII.GetString((byte[])data);
|
||||
|
||||
var type = regKey.GetValueKind(keyName);
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
return Encoding.ASCII.GetString((byte[])data);
|
||||
case RegistryValueKind.DWord:
|
||||
if (data.GetType() != typeof(long))
|
||||
return data;
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
if (data.GetType() != typeof(long))
|
||||
return data;
|
||||
|
||||
long.TryParse(data.ToString(), out var longValue);
|
||||
var bytes = BitConverter.GetBytes(longValue);
|
||||
var doubleValue = BitConverter.ToDouble(bytes, 0);
|
||||
return doubleValue;
|
||||
}
|
||||
long.TryParse(data.ToString(), out var longValue);
|
||||
var bytes = BitConverter.GetBytes(longValue);
|
||||
var doubleValue = BitConverter.ToDouble(bytes, 0);
|
||||
return doubleValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override string GetVRChatRegistryKeyString(string key)
|
||||
{
|
||||
// for electron
|
||||
return GetVRChatRegistryKey(key)?.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the specified key in the VRChat group in the windows registry.
|
||||
/// </summary>
|
||||
@@ -84,33 +88,31 @@ namespace VRCX
|
||||
/// <param name="value">The value to set for the specified key.</param>
|
||||
/// <param name="typeInt">The RegistryValueKind type.</param>
|
||||
/// <returns>True if the key was successfully set, false otherwise.</returns>
|
||||
public bool SetVRChatRegistryKey(string key, object value, int typeInt)
|
||||
public override bool SetVRChatRegistryKey(string key, object value, int typeInt)
|
||||
{
|
||||
var type = (RegistryValueKind)typeInt;
|
||||
var keyName = AddHashToKeyName(key);
|
||||
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat", true))
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat", true);
|
||||
if (regKey == null)
|
||||
return false;
|
||||
|
||||
object setValue = null;
|
||||
switch (type)
|
||||
{
|
||||
if (regKey == null)
|
||||
return false;
|
||||
case RegistryValueKind.Binary:
|
||||
setValue = Encoding.ASCII.GetBytes(value.ToString());
|
||||
break;
|
||||
|
||||
object setValue = null;
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
setValue = Encoding.ASCII.GetBytes(value.ToString());
|
||||
break;
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
setValue = value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (setValue == null)
|
||||
return false;
|
||||
|
||||
regKey.SetValue(keyName, setValue, type);
|
||||
case RegistryValueKind.DWord:
|
||||
setValue = value;
|
||||
break;
|
||||
}
|
||||
|
||||
if (setValue == null)
|
||||
return false;
|
||||
|
||||
regKey.SetValue(keyName, setValue, type);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -119,7 +121,7 @@ namespace VRCX
|
||||
/// </summary>
|
||||
/// <param name="key">The name of the key to set.</param>
|
||||
/// <param name="value">The value to set for the specified key.</param>
|
||||
public void SetVRChatRegistryKey(string key, byte[] value)
|
||||
public override void SetVRChatRegistryKey(string key, byte[] value)
|
||||
{
|
||||
var keyName = AddHashToKeyName(key);
|
||||
var hKey = (UIntPtr)0x80000001; // HKEY_LOCAL_MACHINE
|
||||
@@ -136,78 +138,77 @@ namespace VRCX
|
||||
RegCloseKey(hKey);
|
||||
}
|
||||
|
||||
public Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
|
||||
public override Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
|
||||
{
|
||||
var output = new Dictionary<string, Dictionary<string, object>>();
|
||||
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
if (regKey == null)
|
||||
throw new Exception("Nothing to backup.");
|
||||
|
||||
var keys = regKey.GetValueNames();
|
||||
|
||||
Span<long> spanLong = stackalloc long[1];
|
||||
Span<double> doubleSpan = MemoryMarshal.Cast<long, double>(spanLong);
|
||||
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (regKey == null)
|
||||
throw new Exception("Nothing to backup.");
|
||||
var data = regKey.GetValue(key);
|
||||
var index = key.LastIndexOf("_h", StringComparison.Ordinal);
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
var keys = regKey.GetValueNames();
|
||||
var keyName = key.Substring(0, index);
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
Span<long> spanLong = stackalloc long[1];
|
||||
Span<double> doubleSpan = MemoryMarshal.Cast<long, double>(spanLong);
|
||||
|
||||
foreach (var key in keys)
|
||||
var type = regKey.GetValueKind(key);
|
||||
switch (type)
|
||||
{
|
||||
var data = regKey.GetValue(key);
|
||||
var index = key.LastIndexOf("_h", StringComparison.Ordinal);
|
||||
if (index <= 0)
|
||||
continue;
|
||||
case RegistryValueKind.Binary:
|
||||
var binDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", Encoding.ASCII.GetString((byte[])data) },
|
||||
{ "type", type }
|
||||
};
|
||||
output.Add(keyName, binDict);
|
||||
break;
|
||||
|
||||
var keyName = key.Substring(0, index);
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
var type = regKey.GetValueKind(key);
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
var binDict = new Dictionary<string, object>
|
||||
case RegistryValueKind.DWord:
|
||||
if (data.GetType() != typeof(long))
|
||||
{
|
||||
var dwordDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", Encoding.ASCII.GetString((byte[])data) },
|
||||
{ "data", data },
|
||||
{ "type", type }
|
||||
};
|
||||
output.Add(keyName, binDict);
|
||||
output.Add(keyName, dwordDict);
|
||||
break;
|
||||
}
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
if (data.GetType() != typeof(long))
|
||||
{
|
||||
var dwordDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", data },
|
||||
{ "type", type }
|
||||
};
|
||||
output.Add(keyName, dwordDict);
|
||||
break;
|
||||
}
|
||||
spanLong[0] = (long)data;
|
||||
var doubleValue = doubleSpan[0];
|
||||
var floatDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", doubleValue },
|
||||
{ "type", 100 } // it's special
|
||||
};
|
||||
output.Add(keyName, floatDict);
|
||||
break;
|
||||
|
||||
spanLong[0] = (long)data;
|
||||
var doubleValue = doubleSpan[0];
|
||||
var floatDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", doubleValue },
|
||||
{ "type", 100 } // it's special
|
||||
};
|
||||
output.Add(keyName, floatDict);
|
||||
break;
|
||||
|
||||
default:
|
||||
Debug.WriteLine($"Unknown registry value kind: {type}");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Debug.WriteLine($"Unknown registry value kind: {type}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public void SetVRChatRegistry(string json)
|
||||
public override void SetVRChatRegistry(string json)
|
||||
{
|
||||
CreateVRChatRegistryFolder();
|
||||
Span<double> spanDouble = stackalloc double[1];
|
||||
var dict = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json);
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json);
|
||||
foreach (var item in dict)
|
||||
{
|
||||
var data = (JsonElement)item.Value["data"];
|
||||
@@ -241,45 +242,36 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasVRChatRegistryFolder()
|
||||
public override bool HasVRChatRegistryFolder()
|
||||
{
|
||||
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
|
||||
{
|
||||
return regKey != null;
|
||||
}
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
return regKey != null;
|
||||
}
|
||||
|
||||
public void CreateVRChatRegistryFolder()
|
||||
private void CreateVRChatRegistryFolder()
|
||||
{
|
||||
if (HasVRChatRegistryFolder())
|
||||
return;
|
||||
|
||||
using (var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\VRChat\VRChat"))
|
||||
{
|
||||
if (key == null)
|
||||
throw new Exception("Error creating registry key.");
|
||||
}
|
||||
using var key = Registry.CurrentUser.CreateSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
if (key == null)
|
||||
throw new Exception("Error creating registry key.");
|
||||
}
|
||||
|
||||
public void DeleteVRChatRegistryFolder()
|
||||
public override void DeleteVRChatRegistryFolder()
|
||||
{
|
||||
using (var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat"))
|
||||
{
|
||||
if (regKey == null)
|
||||
return;
|
||||
using var regKey = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\VRChat\VRChat");
|
||||
if (regKey == null)
|
||||
return;
|
||||
|
||||
Registry.CurrentUser.DeleteSubKeyTree(@"SOFTWARE\VRChat\VRChat");
|
||||
}
|
||||
Registry.CurrentUser.DeleteSubKeyTree(@"SOFTWARE\VRChat\VRChat");
|
||||
}
|
||||
|
||||
|
||||
public string ReadVrcRegJsonFile(string filepath)
|
||||
|
||||
public override string ReadVrcRegJsonFile(string filepath)
|
||||
{
|
||||
if (!File.Exists(filepath))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
||||
var json = File.ReadAllText(filepath);
|
||||
return json;
|
||||
}
|
||||
40
Dotnet/AppApi/Cef/Screenshot.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiCef
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the PNG screenshot file.</param>
|
||||
/// <param name="metadataString">The metadata to add to the screenshot file.</param>
|
||||
/// <param name="worldId">The ID of the world to associate with the screenshot.</param>
|
||||
/// <param name="changeFilename">Whether to rename the screenshot file to include the world ID.</param>
|
||||
public override string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
if (!File.Exists(path) || !path.EndsWith(".png") || !fileName.StartsWith("VRChat_"))
|
||||
return string.Empty;
|
||||
|
||||
if (changeFilename)
|
||||
{
|
||||
var newFileName = $"{fileName}_{worldId}";
|
||||
var newPath = Path.Combine(Path.GetDirectoryName(path), newFileName + Path.GetExtension(path));
|
||||
File.Move(path, newPath);
|
||||
path = newPath;
|
||||
}
|
||||
|
||||
ScreenshotHelper.WritePNGDescription(path, metadataString);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
Dotnet/AppApi/Common/AppApiCommon.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using librsync.net;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly MD5 _hasher = MD5.Create();
|
||||
|
||||
public void Init()
|
||||
{
|
||||
}
|
||||
|
||||
public string MD5File(string blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64CharArray(blob.ToCharArray(), 0, blob.Length);
|
||||
using var md5 = MD5.Create();
|
||||
var md5Hash = md5.ComputeHash(fileData);
|
||||
return Convert.ToBase64String(md5Hash);
|
||||
}
|
||||
|
||||
public int GetColourFromUserID(string userId)
|
||||
{
|
||||
var hash = _hasher.ComputeHash(Encoding.UTF8.GetBytes(userId));
|
||||
return (hash[3] << 8) | hash[4];
|
||||
}
|
||||
|
||||
public string SignFile(string blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64String(blob);
|
||||
using var sig = Librsync.ComputeSignature(new MemoryStream(fileData));
|
||||
using var memoryStream = new MemoryStream();
|
||||
sig.CopyTo(memoryStream);
|
||||
var sigBytes = memoryStream.ToArray();
|
||||
return Convert.ToBase64String(sigBytes);
|
||||
}
|
||||
|
||||
public string FileLength(string blob)
|
||||
{
|
||||
var fileData = Convert.FromBase64String(blob);
|
||||
return fileData.Length.ToString();
|
||||
}
|
||||
|
||||
public void OpenLink(string url)
|
||||
{
|
||||
if (url.StartsWith("http://") ||
|
||||
url.StartsWith("https://"))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(url)
|
||||
{
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void IPCAnnounceStart()
|
||||
{
|
||||
IPCServer.Send(new IPCPacket
|
||||
{
|
||||
Type = "VRCXLaunch",
|
||||
MsgType = "VRCXLaunch"
|
||||
});
|
||||
}
|
||||
|
||||
public void SendIpc(string type, string data)
|
||||
{
|
||||
IPCServer.Send(new IPCPacket
|
||||
{
|
||||
Type = "VrcxMessage",
|
||||
MsgType = type,
|
||||
Data = data
|
||||
});
|
||||
}
|
||||
|
||||
public string CustomCssPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.css");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
public string CustomScriptPath()
|
||||
{
|
||||
var output = string.Empty;
|
||||
var filePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX\\custom.js");
|
||||
if (File.Exists(filePath))
|
||||
output = filePath;
|
||||
return output;
|
||||
}
|
||||
|
||||
public string CurrentCulture()
|
||||
{
|
||||
return CultureInfo.CurrentCulture.ToString();
|
||||
}
|
||||
|
||||
public string CurrentLanguage()
|
||||
{
|
||||
return CultureInfo.InstalledUICulture.Name;
|
||||
}
|
||||
|
||||
public string GetVersion()
|
||||
{
|
||||
return Program.Version;
|
||||
}
|
||||
|
||||
public bool VrcClosedGracefully()
|
||||
{
|
||||
return LogWatcher.Instance.VrcClosedGracefully;
|
||||
}
|
||||
|
||||
public Dictionary<string, int> GetColourBulk(List<object> userIds)
|
||||
{
|
||||
var output = new Dictionary<string, int>();
|
||||
foreach (string userId in userIds)
|
||||
{
|
||||
output.Add(userId, GetColourFromUserID(userId));
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public void SetAppLauncherSettings(bool enabled, bool killOnExit)
|
||||
{
|
||||
AutoAppLaunchManager.Instance.Enabled = enabled;
|
||||
AutoAppLaunchManager.Instance.KillChildrenOnExit = killOnExit;
|
||||
}
|
||||
|
||||
public string GetFileBase64(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
return Convert.ToBase64String(File.ReadAllBytes(path));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Dotnet/AppApi/Common/AppApiCommonBase.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public abstract partial class AppApi
|
||||
{
|
||||
// AppApi
|
||||
public abstract void ShowDevTools();
|
||||
public abstract void DeleteAllCookies();
|
||||
public abstract void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand);
|
||||
public abstract void RefreshVR();
|
||||
public abstract void RestartVR();
|
||||
public abstract void SetZoom(double zoomLevel);
|
||||
public abstract Task<double> GetZoom();
|
||||
public abstract void DesktopNotification(string BoldText, string Text = "", string Image = "");
|
||||
|
||||
public abstract void RestartApplication(bool isUpgrade);
|
||||
public abstract bool CheckForUpdateExe();
|
||||
public abstract void ExecuteAppFunction(string function, string json);
|
||||
public abstract void ExecuteVrFeedFunction(string function, string json);
|
||||
public abstract void ExecuteVrOverlayFunction(string function, string json);
|
||||
public abstract string GetLaunchCommand();
|
||||
public abstract void FocusWindow();
|
||||
public abstract void ChangeTheme(int value);
|
||||
public abstract void DoFunny();
|
||||
public abstract string GetClipboard();
|
||||
public abstract void SetStartup(bool enabled);
|
||||
public abstract void CopyImageToClipboard(string path);
|
||||
public abstract void FlashWindow();
|
||||
public abstract void SetUserAgent();
|
||||
public abstract bool IsRunningUnderWine();
|
||||
|
||||
// Folders
|
||||
public abstract string GetVRChatAppDataLocation();
|
||||
public abstract string GetVRChatPhotosLocation();
|
||||
public abstract string GetUGCPhotoLocation(string path = "");
|
||||
public abstract string GetVRChatScreenshotsLocation();
|
||||
public abstract string GetVRChatCacheLocation();
|
||||
public abstract bool OpenVrcxAppDataFolder();
|
||||
public abstract bool OpenVrcAppDataFolder();
|
||||
public abstract bool OpenVrcPhotosFolder();
|
||||
public abstract bool OpenUGCPhotosFolder(string ugcPath = "");
|
||||
public abstract bool OpenVrcScreenshotsFolder();
|
||||
public abstract bool OpenCrashVrcCrashDumps();
|
||||
public abstract void OpenShortcutFolder();
|
||||
public abstract void OpenFolderAndSelectItem(string path, bool isFolder = false);
|
||||
public abstract Task<string> OpenFolderSelectorDialog(string defaultPath = "");
|
||||
|
||||
public abstract Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "",
|
||||
string defaultFilter = "All files (*.*)|*.*");
|
||||
|
||||
// GameHandler
|
||||
public abstract void OnProcessStateChanged(MonitoredProcess monitoredProcess);
|
||||
public abstract void CheckGameRunning();
|
||||
public abstract bool IsGameRunning();
|
||||
public abstract bool IsSteamVRRunning();
|
||||
public abstract int QuitGame();
|
||||
public abstract bool StartGame(string arguments);
|
||||
public abstract bool StartGameFromPath(string path, string arguments);
|
||||
|
||||
// RegistryPlayerPrefs
|
||||
public abstract object GetVRChatRegistryKey(string key);
|
||||
public abstract string GetVRChatRegistryKeyString(string key);
|
||||
public abstract bool SetVRChatRegistryKey(string key, object value, int typeInt);
|
||||
public abstract void SetVRChatRegistryKey(string key, byte[] value);
|
||||
public abstract Dictionary<string, Dictionary<string, object>> GetVRChatRegistry();
|
||||
public abstract void SetVRChatRegistry(string json);
|
||||
public abstract bool HasVRChatRegistryFolder();
|
||||
public abstract void DeleteVRChatRegistryFolder();
|
||||
public abstract string ReadVrcRegJsonFile(string filepath);
|
||||
|
||||
// Screenshot
|
||||
public abstract string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false);
|
||||
}
|
||||
}
|
||||
237
Dotnet/AppApi/Common/ImageSaving.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public async Task<string> GetImage(string url, string fileId, string version)
|
||||
{
|
||||
return await ImageCache.GetImage(url, fileId, version);
|
||||
}
|
||||
|
||||
public string ResizeImageToFitLimits(string base64data)
|
||||
{
|
||||
return Convert.ToBase64String(ResizeImageToFitLimits(Convert.FromBase64String(base64data), false));
|
||||
}
|
||||
|
||||
public byte[] ResizeImageToFitLimits(byte[] imageData, bool matchingDimensions, int maxWidth = 2000, int maxHeight = 2000, long maxSize = 10_000_000)
|
||||
{
|
||||
using var fileMemoryStream = new MemoryStream(imageData);
|
||||
var image = new Bitmap(fileMemoryStream);
|
||||
|
||||
// for APNG, check if image is png format and less than maxSize
|
||||
if ((!matchingDimensions || image.Width == image.Height) &&
|
||||
image.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Png) &&
|
||||
imageData.Length < maxSize &&
|
||||
image.Width <= maxWidth &&
|
||||
image.Height <= maxHeight)
|
||||
{
|
||||
return imageData;
|
||||
}
|
||||
|
||||
if (image.Width > maxWidth)
|
||||
{
|
||||
var sizingFactor = image.Width / (double)maxWidth;
|
||||
var newHeight = (int)Math.Round(image.Height / sizingFactor);
|
||||
image = new Bitmap(image, maxWidth, newHeight);
|
||||
}
|
||||
if (image.Height > maxHeight)
|
||||
{
|
||||
var sizingFactor = image.Height / (double)maxHeight;
|
||||
var newWidth = (int)Math.Round(image.Width / sizingFactor);
|
||||
image = new Bitmap(image, newWidth, maxHeight);
|
||||
}
|
||||
if (matchingDimensions && image.Width != image.Height)
|
||||
{
|
||||
var newSize = Math.Max(image.Width, image.Height);
|
||||
var newImage = new Bitmap(newSize, newSize);
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
graphics.Clear(Color.Transparent);
|
||||
graphics.DrawImage(image, new Rectangle((newSize - image.Width) / 2, (newSize - image.Height) / 2, image.Width, image.Height));
|
||||
image.Dispose();
|
||||
image = newImage;
|
||||
}
|
||||
|
||||
SaveToFileToUpload();
|
||||
for (int i = 0; i < 250 && imageData.Length > maxSize; i++)
|
||||
{
|
||||
SaveToFileToUpload();
|
||||
if (imageData.Length < maxSize)
|
||||
break;
|
||||
|
||||
int newWidth;
|
||||
int newHeight;
|
||||
if (image.Width > image.Height)
|
||||
{
|
||||
newWidth = image.Width - 25;
|
||||
newHeight = (int)Math.Round(image.Height / (image.Width / (double)newWidth));
|
||||
}
|
||||
else
|
||||
{
|
||||
newHeight = image.Height - 25;
|
||||
newWidth = (int)Math.Round(image.Width / (image.Height / (double)newHeight));
|
||||
}
|
||||
image = new Bitmap(image, newWidth, newHeight);
|
||||
}
|
||||
|
||||
if (imageData.Length > maxSize)
|
||||
{
|
||||
throw new Exception("Failed to get image into target filesize.");
|
||||
}
|
||||
|
||||
return imageData;
|
||||
|
||||
void SaveToFileToUpload()
|
||||
{
|
||||
using var imageSaveMemoryStream = new MemoryStream();
|
||||
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
imageData = imageSaveMemoryStream.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] ResizePrintImage(byte[] imageData)
|
||||
{
|
||||
const int desiredWidth = 1920;
|
||||
const int desiredHeight = 1080;
|
||||
|
||||
using var fileMemoryStream = new MemoryStream(imageData);
|
||||
var image = new Bitmap(fileMemoryStream);
|
||||
|
||||
if (image.Height > image.Width)
|
||||
image.RotateFlip(RotateFlipType.Rotate90FlipNone);
|
||||
|
||||
// increase size to 1920x1080
|
||||
if (image.Width < desiredWidth || image.Height < desiredHeight)
|
||||
{
|
||||
var newHeight = image.Height;
|
||||
var newWidth = image.Width;
|
||||
if (image.Width < desiredWidth)
|
||||
{
|
||||
var testHeight = (int)Math.Round(image.Height / (image.Width / (double)desiredWidth));
|
||||
if (testHeight <= desiredHeight)
|
||||
{
|
||||
newWidth = desiredWidth;
|
||||
newHeight = testHeight;
|
||||
}
|
||||
}
|
||||
if (image.Height < desiredHeight)
|
||||
{
|
||||
var testWidth = (int)Math.Round(image.Width / (image.Height / (double)desiredHeight));
|
||||
if (testWidth <= desiredWidth)
|
||||
{
|
||||
newHeight = desiredHeight;
|
||||
newWidth = testWidth;
|
||||
}
|
||||
}
|
||||
var resizedImage = new Bitmap(desiredWidth, desiredHeight);
|
||||
using var graphics1 = Graphics.FromImage(resizedImage);
|
||||
graphics1.Clear(Color.White);
|
||||
var x = (desiredWidth - newWidth) / 2;
|
||||
var y = (desiredHeight - newHeight) / 2;
|
||||
graphics1.DrawImage(image, new Rectangle(x, y, newWidth, newHeight));
|
||||
image.Dispose();
|
||||
image = resizedImage;
|
||||
}
|
||||
|
||||
// limit size to 1920x1080
|
||||
if (image.Width > desiredWidth)
|
||||
{
|
||||
var sizingFactor = image.Width / (double)desiredWidth;
|
||||
var newHeight = (int)Math.Round(image.Height / sizingFactor);
|
||||
image = new Bitmap(image, desiredWidth, newHeight);
|
||||
}
|
||||
if (image.Height > desiredHeight)
|
||||
{
|
||||
var sizingFactor = image.Height / (double)desiredHeight;
|
||||
var newWidth = (int)Math.Round(image.Width / sizingFactor);
|
||||
image = new Bitmap(image, newWidth, desiredHeight);
|
||||
}
|
||||
|
||||
// add white border
|
||||
// wtf are these magic numbers
|
||||
const int xOffset = 64; // 2048 / 32
|
||||
const int yOffset = 69; // 1440 / 20.869
|
||||
var newImage = new Bitmap(2048, 1440);
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
graphics.Clear(Color.White);
|
||||
// graphics.DrawImage(image, new Rectangle(xOffset, yOffset, image.Width, image.Height));
|
||||
var newX = (2048 - image.Width) / 2;
|
||||
var newY = yOffset;
|
||||
graphics.DrawImage(image, new Rectangle(newX, newY, image.Width, image.Height));
|
||||
image.Dispose();
|
||||
image = newImage;
|
||||
|
||||
using var imageSaveMemoryStream = new MemoryStream();
|
||||
image.Save(imageSaveMemoryStream, System.Drawing.Imaging.ImageFormat.Png);
|
||||
return imageSaveMemoryStream.ToArray();
|
||||
}
|
||||
|
||||
public async Task CropAllPrints(string ugcFolderPath)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints");
|
||||
var files = Directory.GetFiles(folder, "*.png", SearchOption.AllDirectories);
|
||||
foreach (var file in files)
|
||||
{
|
||||
await CropPrintImage(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> CropPrintImage(string path)
|
||||
{
|
||||
var tempPath = path + ".temp";
|
||||
var bytes = await File.ReadAllBytesAsync(path);
|
||||
var ms = new MemoryStream(bytes);
|
||||
Bitmap print = new Bitmap(ms);
|
||||
// validation step to ensure image is actually a print
|
||||
if (print.Width != 2048 || print.Height != 1440)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var point = new Point(64, 69);
|
||||
var size = new Size(1920, 1080);
|
||||
var rectangle = new Rectangle(point, size);
|
||||
Bitmap cropped = print.Clone(rectangle, print.PixelFormat);
|
||||
cropped.Save(tempPath);
|
||||
if (ScreenshotHelper.HasTXt(path))
|
||||
{
|
||||
var success = ScreenshotHelper.CopyTXt(path, tempPath);
|
||||
if (!success)
|
||||
{
|
||||
File.Delete(tempPath);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File.Move(tempPath, path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<string> SavePrintToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Prints", MakeValidFileName(monthFolder));
|
||||
Directory.CreateDirectory(folder);
|
||||
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
|
||||
if (File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var success = await ImageCache.SaveImageToFile(url, filePath);
|
||||
|
||||
return success ? filePath : null;
|
||||
}
|
||||
|
||||
public async Task<string> SaveStickerToFile(string url, string ugcFolderPath, string monthFolder, string fileName)
|
||||
{
|
||||
var folder = Path.Combine(GetUGCPhotoLocation(ugcFolderPath), "Stickers", MakeValidFileName(monthFolder));
|
||||
Directory.CreateDirectory(folder);
|
||||
var filePath = Path.Combine(folder, MakeValidFileName(fileName));
|
||||
if (File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var success = await ImageCache.SaveImageToFile(url, filePath);
|
||||
|
||||
return success ? filePath : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
92
Dotnet/AppApi/Common/LocalPlayerModerations.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public Dictionary<string, short> GetVRChatModerations(string currentUserId)
|
||||
{
|
||||
// 004 = hideAvatar
|
||||
// 005 = showAvatar
|
||||
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
|
||||
if (!File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var output = new Dictionary<string, short>();
|
||||
using var reader = new StreamReader(filePath);
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var index = line.IndexOf(' ');
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
var userId = line.Substring(0, index);
|
||||
var type = short.Parse(line.Substring(line.Length - 3));
|
||||
output.Add(userId, type);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public short GetVRChatUserModeration(string currentUserId, string userId)
|
||||
{
|
||||
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
|
||||
if (!File.Exists(filePath))
|
||||
return 0;
|
||||
|
||||
using var reader = new StreamReader(filePath);
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var index = line.IndexOf(' ');
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
if (userId == line.Substring(0, index))
|
||||
{
|
||||
return short.Parse(line.Substring(line.Length - 3));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public bool SetVRChatUserModeration(string currentUserId, string userId, int type)
|
||||
{
|
||||
var filePath = Path.Combine(GetVRChatAppDataLocation(), @$"LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset");
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
var lines = File.ReadAllLines(filePath).ToList();
|
||||
var index = lines.FindIndex(x => x.StartsWith(userId));
|
||||
if (index >= 0)
|
||||
lines.RemoveAt(index);
|
||||
|
||||
if (type != 0)
|
||||
{
|
||||
var sb = new StringBuilder(userId);
|
||||
while (sb.Length < 64)
|
||||
sb.Append(' ');
|
||||
|
||||
sb.Append(type.ToString("000"));
|
||||
lines.Add(sb.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(filePath, lines);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text.Json;
|
||||
using Websocket.Client;
|
||||
|
||||
namespace VRCX
|
||||
@@ -14,7 +15,7 @@ namespace VRCX
|
||||
private static readonly object _ovrtLock = new();
|
||||
private static WebsocketClient _ovrtWebsocketClient;
|
||||
|
||||
private static void Initialize()
|
||||
private static void InitializeOvrTk()
|
||||
{
|
||||
lock (_ovrtLock)
|
||||
{
|
||||
@@ -62,16 +63,16 @@ namespace VRCX
|
||||
|
||||
private static void SendMessages(IEnumerable<OvrtMessage> ovrtMessages)
|
||||
{
|
||||
if(ovrtMessages != null && ovrtMessages.Any())
|
||||
if (ovrtMessages != null && ovrtMessages.Any())
|
||||
{
|
||||
if (_ovrtWebsocketClient == null)
|
||||
Initialize();
|
||||
InitializeOvrTk();
|
||||
|
||||
if (_ovrtWebsocketClient.IsRunning)
|
||||
{
|
||||
foreach (var message in ovrtMessages)
|
||||
{
|
||||
_ovrtWebsocketClient.Send(System.Text.Json.JsonSerializer.Serialize(message));
|
||||
_ovrtWebsocketClient.Send(JsonSerializer.Serialize(message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +108,7 @@ namespace VRCX
|
||||
messages.Add(new OvrtMessage
|
||||
{
|
||||
MessageType = "SendWristNotification",
|
||||
Json = System.Text.Json.JsonSerializer.Serialize(new OvrtWristNotificationMessage
|
||||
Json = JsonSerializer.Serialize(new OvrtWristNotificationMessage
|
||||
{
|
||||
Body = title + " - " + body
|
||||
})
|
||||
@@ -119,7 +120,7 @@ namespace VRCX
|
||||
messages.Add(new OvrtMessage
|
||||
{
|
||||
MessageType = "SendNotification",
|
||||
Json = System.Text.Json.JsonSerializer.Serialize(new OvrtHudNotificationMessage
|
||||
Json = JsonSerializer.Serialize(new OvrtHudNotificationMessage
|
||||
{
|
||||
Title = title,
|
||||
Body = body,
|
||||
@@ -130,6 +131,7 @@ namespace VRCX
|
||||
|
||||
SendMessages(messages);
|
||||
}
|
||||
|
||||
private struct OvrtMessage
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("messageType")]
|
||||
@@ -2,41 +2,15 @@ using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace VRCX
|
||||
namespace VRCX;
|
||||
|
||||
public partial class AppApi
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds metadata to a PNG screenshot file and optionally renames the file to include the specified world ID.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the PNG screenshot file.</param>
|
||||
/// <param name="metadataString">The metadata to add to the screenshot file.</param>
|
||||
/// <param name="worldId">The ID of the world to associate with the screenshot.</param>
|
||||
/// <param name="changeFilename">Whether or not to rename the screenshot file to include the world ID.</param>
|
||||
public string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
if (!File.Exists(path) || !path.EndsWith(".png") || !fileName.StartsWith("VRChat_"))
|
||||
return string.Empty;
|
||||
|
||||
if (changeFilename)
|
||||
{
|
||||
var newFileName = $"{fileName}_{worldId}";
|
||||
var newPath = Path.Combine(Path.GetDirectoryName(path), newFileName + Path.GetExtension(path));
|
||||
File.Move(path, newPath);
|
||||
path = newPath;
|
||||
}
|
||||
|
||||
ScreenshotHelper.WritePNGDescription(path, metadataString);
|
||||
return path;
|
||||
}
|
||||
|
||||
public string GetExtraScreenshotData(string path, bool carouselCache)
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
@@ -75,15 +49,12 @@ namespace VRCX
|
||||
return metadata.ToString(Formatting.Indented);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves metadata from a PNG screenshot file and send the result to displayScreenshotMetadata in app.js
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the PNG screenshot file.</param>
|
||||
public string GetScreenshotMetadata(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return null;
|
||||
|
||||
|
||||
var metadata = ScreenshotHelper.GetScreenshotMetadata(path);
|
||||
|
||||
if (metadata == null)
|
||||
@@ -139,9 +110,6 @@ namespace VRCX
|
||||
return json.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets and returns the path of the last screenshot taken by VRChat.
|
||||
/// </summary>
|
||||
public string GetLastScreenshot()
|
||||
{
|
||||
// Get the last screenshot taken by VRChat
|
||||
@@ -159,5 +127,4 @@ namespace VRCX
|
||||
|
||||
return lastScreenshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Dotnet/AppApi/Common/Update.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public partial class AppApi
|
||||
{
|
||||
public async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize)
|
||||
{
|
||||
await Update.DownloadUpdate(fileUrl, fileName, hashUrl, downloadSize);
|
||||
}
|
||||
|
||||
public void CancelUpdate()
|
||||
{
|
||||
Update.CancelUpdate();
|
||||
}
|
||||
|
||||
public int CheckUpdateProgress()
|
||||
{
|
||||
return Update.UpdateProgress;
|
||||
}
|
||||
}
|
||||
23
Dotnet/AppApi/Common/Utils.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public partial class AppApi
|
||||
{
|
||||
private static readonly Regex _folderRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
|
||||
Regex.Escape(new string(Path.GetInvalidPathChars()))));
|
||||
|
||||
private static readonly Regex _fileRegex = new Regex(string.Format(@"([{0}]*\.+$)|([{0}]+)",
|
||||
Regex.Escape(new string(Path.GetInvalidFileNameChars()))));
|
||||
|
||||
private static string MakeValidFileName(string name)
|
||||
{
|
||||
name = name.Replace("/", "");
|
||||
name = name.Replace("\\", "");
|
||||
name = _folderRegex.Replace(name, "");
|
||||
name = _fileRegex.Replace(name, "");
|
||||
|
||||
return name;
|
||||
}
|
||||
}
|
||||
28
Dotnet/AppApi/Common/VrcConfigFile.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
public string ReadConfigFile()
|
||||
{
|
||||
var path = GetVRChatAppDataLocation();
|
||||
var configFile = Path.Combine(path, "config.json");
|
||||
if (!Directory.Exists(path) || !File.Exists(configFile))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(configFile);
|
||||
return json;
|
||||
}
|
||||
|
||||
public void WriteConfigFile(string json)
|
||||
{
|
||||
var path = GetVRChatAppDataLocation();
|
||||
var configFile = Path.Combine(path, "config.json");
|
||||
File.WriteAllText(configFile, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
@@ -45,7 +46,7 @@ namespace VRCX
|
||||
icon = icon
|
||||
};
|
||||
|
||||
var byteBuffer = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(msg);
|
||||
var byteBuffer = JsonSerializer.SerializeToUtf8Bytes(msg);
|
||||
broadcastSocket.SendTo(byteBuffer, endPoint);
|
||||
broadcastSocket.Close();
|
||||
}
|
||||
140
Dotnet/AppApi/Electron/AppApiElectron.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiElectron : AppApi
|
||||
{
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
public override void DeleteAllCookies()
|
||||
{
|
||||
}
|
||||
|
||||
public override void ShowDevTools()
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetVR(bool active, bool hmdOverlay, bool wristOverlay, bool menuButton, int overlayHand)
|
||||
{
|
||||
}
|
||||
|
||||
public override void RefreshVR()
|
||||
{
|
||||
}
|
||||
|
||||
public override void RestartVR()
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetZoom(double zoomLevel)
|
||||
{
|
||||
}
|
||||
|
||||
public override async Task<double> GetZoom()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override void DesktopNotification(string BoldText, string Text = "", string Image = "")
|
||||
{
|
||||
}
|
||||
|
||||
public override void RestartApplication(bool isUpgrade)
|
||||
{
|
||||
}
|
||||
|
||||
public override bool CheckForUpdateExe()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void ExecuteAppFunction(string function, string json)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ExecuteVrFeedFunction(string function, string json)
|
||||
{
|
||||
}
|
||||
|
||||
public override void ExecuteVrOverlayFunction(string function, string json)
|
||||
{
|
||||
}
|
||||
|
||||
public override string GetLaunchCommand()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override void FocusWindow()
|
||||
{
|
||||
}
|
||||
|
||||
public override void ChangeTheme(int value)
|
||||
{
|
||||
}
|
||||
|
||||
public override void DoFunny()
|
||||
{
|
||||
}
|
||||
|
||||
public override string GetClipboard()
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override void SetStartup(bool enabled)
|
||||
{
|
||||
}
|
||||
|
||||
public override void CopyImageToClipboard(string path)
|
||||
{
|
||||
if (!File.Exists(path) ||
|
||||
(!path.EndsWith(".png") &&
|
||||
!path.EndsWith(".jpg") &&
|
||||
!path.EndsWith(".jpeg") &&
|
||||
!path.EndsWith(".gif") &&
|
||||
!path.EndsWith(".bmp") &&
|
||||
!path.EndsWith(".webp")))
|
||||
return;
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "xclip",
|
||||
Arguments = $"-selection clipboard -t image/png -i \"{path}\"",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Failed to copy image to clipboard: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void FlashWindow()
|
||||
{
|
||||
}
|
||||
|
||||
public override void SetUserAgent()
|
||||
{
|
||||
}
|
||||
|
||||
public override bool IsRunningUnderWine()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
196
Dotnet/AppApi/Electron/Folders.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Microsoft.Win32;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiElectron
|
||||
{
|
||||
public static string _homeDirectory;
|
||||
public static string _steamPath;
|
||||
public static string _steamUserdataPath;
|
||||
public static string _vrcPrefixPath;
|
||||
public static string _vrcAppDataPath;
|
||||
|
||||
static AppApiElectron()
|
||||
{
|
||||
_homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
_steamPath = Path.Combine(_homeDirectory, ".local/share/Steam");
|
||||
var flatpakSteamPath = Path.Combine(_homeDirectory, ".var/app/com.valvesoftware.Steam/.local/share/Steam");
|
||||
if (!Directory.Exists(_steamPath) && Directory.Exists(flatpakSteamPath))
|
||||
{
|
||||
logger.Info("Flatpak Steam detected.");
|
||||
_steamPath = flatpakSteamPath;
|
||||
}
|
||||
_steamUserdataPath = Path.Combine(_homeDirectory, ".steam/steam/userdata");
|
||||
_vrcPrefixPath = Path.Combine(_steamPath, "steamapps/compatdata/438100/pfx");
|
||||
_vrcAppDataPath = Path.Combine(_vrcPrefixPath, "drive_c/users/steamuser/AppData/LocalLow/VRChat/VRChat");
|
||||
}
|
||||
|
||||
public override string GetVRChatAppDataLocation()
|
||||
{
|
||||
return _vrcAppDataPath;
|
||||
}
|
||||
|
||||
public override string GetVRChatCacheLocation()
|
||||
{
|
||||
var json = ReadConfigFile();
|
||||
if (!string.IsNullOrEmpty(json))
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject<JObject>(json);
|
||||
if (obj["cache_directory"] != null)
|
||||
{
|
||||
var cacheDir = (string)obj["cache_directory"];
|
||||
if (!string.IsNullOrEmpty(cacheDir) && Directory.Exists(cacheDir))
|
||||
{
|
||||
return cacheDir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Path.Combine(GetVRChatAppDataLocation(), "Cache-WindowsPlayer");
|
||||
}
|
||||
|
||||
public override string GetVRChatPhotosLocation()
|
||||
{
|
||||
return Path.Combine(_vrcPrefixPath, "drive_c/users/steamuser/Pictures/VRChat");
|
||||
}
|
||||
|
||||
public override string GetUGCPhotoLocation(string path = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return GetVRChatPhotosLocation();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.Error(e);
|
||||
return GetVRChatPhotosLocation();
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSteamUserdataPathFromRegistry()
|
||||
{
|
||||
// TODO: Fix Steam userdata path, for now just get the first folder
|
||||
if (Directory.Exists(_steamUserdataPath))
|
||||
{
|
||||
var steamUserDirs = Directory.GetDirectories(_steamUserdataPath);
|
||||
if (steamUserDirs.Length > 0)
|
||||
{
|
||||
return steamUserDirs[0];
|
||||
}
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override string GetVRChatScreenshotsLocation()
|
||||
{
|
||||
// program files steam userdata screenshots
|
||||
return Path.Combine(_steamUserdataPath, "760/remote/438100/screenshots");
|
||||
}
|
||||
|
||||
public override bool OpenVrcxAppDataFolder()
|
||||
{
|
||||
var path = Program.AppDataDirectory;
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OpenVrcAppDataFolder()
|
||||
{
|
||||
var path = _vrcAppDataPath;
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OpenVrcPhotosFolder()
|
||||
{
|
||||
var path = GetVRChatPhotosLocation();
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OpenUGCPhotosFolder(string ugcPath = "")
|
||||
{
|
||||
var path = GetUGCPhotoLocation(ugcPath);
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OpenVrcScreenshotsFolder()
|
||||
{
|
||||
var path = GetVRChatScreenshotsLocation();
|
||||
if (!Directory.Exists(path))
|
||||
return false;
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool OpenCrashVrcCrashDumps()
|
||||
{
|
||||
// TODO: get path
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void OpenShortcutFolder()
|
||||
{
|
||||
var path = AutoAppLaunchManager.Instance.AppShortcutDirectory;
|
||||
if (!Directory.Exists(path))
|
||||
return;
|
||||
|
||||
OpenFolderAndSelectItem(path, true);
|
||||
}
|
||||
|
||||
public override void OpenFolderAndSelectItem(string path, bool isFolder = false)
|
||||
{
|
||||
path = Path.GetFullPath(path);
|
||||
if (!File.Exists(path) && !Directory.Exists(path))
|
||||
return;
|
||||
|
||||
Process.Start("xdg-open", path);
|
||||
}
|
||||
|
||||
public override async Task<string> OpenFolderSelectorDialog(string defaultPath = "")
|
||||
{
|
||||
// TODO: Implement
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public override async Task<string> OpenFileSelectorDialog(string defaultPath = "", string defaultExt = "",
|
||||
string defaultFilter = "All files (*.*)|*.*")
|
||||
{
|
||||
// TODO: Implement
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Dotnet/AppApi/Electron/GameHandler.cs
Normal file
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiElectron
|
||||
{
|
||||
public override void OnProcessStateChanged(MonitoredProcess monitoredProcess)
|
||||
{
|
||||
// unused
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the VRChat game and SteamVR are currently running and updates the browser's JavaScript function $app.updateIsGameRunning with the results.
|
||||
/// </summary>
|
||||
public override void CheckGameRunning()
|
||||
{
|
||||
var isGameRunning = false;
|
||||
var isSteamVRRunning = false;
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("VRChat"))
|
||||
{
|
||||
isGameRunning = true;
|
||||
}
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("vrserver"))
|
||||
{
|
||||
isSteamVRRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsGameRunning()
|
||||
{
|
||||
var isGameRunning = false;
|
||||
var processes = Process.GetProcesses();
|
||||
foreach (var process in processes)
|
||||
{
|
||||
if (process.ProcessName == "VRChat.exe")
|
||||
{
|
||||
isGameRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isGameRunning;
|
||||
}
|
||||
|
||||
public override bool IsSteamVRRunning()
|
||||
{
|
||||
var isSteamVRRunning = false;
|
||||
var processes = Process.GetProcesses();
|
||||
foreach (var process in processes)
|
||||
{
|
||||
if (process.ProcessName == "vrmonitor" || process.ProcessName == "monado-service")
|
||||
{
|
||||
isSteamVRRunning = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isSteamVRRunning;
|
||||
}
|
||||
|
||||
public override int QuitGame()
|
||||
{
|
||||
var processes = Process.GetProcessesByName("vrchat");
|
||||
if (processes.Length == 1)
|
||||
processes[0].Kill();
|
||||
|
||||
return processes.Length;
|
||||
}
|
||||
|
||||
public override bool StartGame(string arguments)
|
||||
{
|
||||
try
|
||||
{
|
||||
var steamPath = _steamPath;
|
||||
if (string.IsNullOrEmpty(steamPath))
|
||||
{
|
||||
logger.Error("Steam path could not be determined.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var steamExecutable = Path.Combine(steamPath, "steam.sh");
|
||||
if (!File.Exists(steamExecutable))
|
||||
{
|
||||
logger.Error("Steam executable not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = steamExecutable,
|
||||
Arguments = $"-applaunch 438100 {arguments}",
|
||||
UseShellExecute = false,
|
||||
})?.Close();
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Failed to start VRChat: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool StartGameFromPath(string path, string arguments)
|
||||
{
|
||||
if (!path.EndsWith(".exe"))
|
||||
path = Path.Combine(path, "launch.exe");
|
||||
|
||||
if (!path.EndsWith("launch.exe") || !File.Exists(path))
|
||||
return false;
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(path),
|
||||
FileName = path,
|
||||
UseShellExecute = false,
|
||||
Arguments = arguments
|
||||
})?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
787
Dotnet/AppApi/Electron/RegistryPlayerPrefs.cs
Normal file
@@ -0,0 +1,787 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiElectron
|
||||
{
|
||||
private string AddHashToKeyName(string key)
|
||||
{
|
||||
// https://discussions.unity.com/t/playerprefs-changing-the-name-of-keys/30332/4
|
||||
// VRC_GROUP_ORDER_usr_032383a7-748c-4fb2-94e4-bcb928e5de6b_h2810492971
|
||||
uint hash = 5381;
|
||||
foreach (var c in key)
|
||||
hash = (hash * 33) ^ c;
|
||||
return key + "_h" + hash;
|
||||
}
|
||||
|
||||
private static int FindMatchingBracket(string content, int openBracketIndex)
|
||||
{
|
||||
int depth = 0;
|
||||
for (int i = openBracketIndex; i < content.Length; i++)
|
||||
{
|
||||
if (content[i] == '{')
|
||||
depth++;
|
||||
else if (content[i] == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> ExtractCompatToolMapping(string vdfContent)
|
||||
{
|
||||
var compatToolMapping = new Dictionary<string, string>();
|
||||
const string sectionHeader = "\"CompatToolMapping\"";
|
||||
int sectionStart = vdfContent.IndexOf(sectionHeader);
|
||||
|
||||
if (sectionStart == -1)
|
||||
{
|
||||
logger.Error("CompatToolMapping not found");
|
||||
return compatToolMapping;
|
||||
}
|
||||
|
||||
int blockStart = vdfContent.IndexOf("{", sectionStart) + 1;
|
||||
int blockEnd = FindMatchingBracket(vdfContent, blockStart - 1);
|
||||
|
||||
if (blockStart == -1 || blockEnd == -1)
|
||||
{
|
||||
logger.Error("CompatToolMapping block not found");
|
||||
return compatToolMapping;
|
||||
}
|
||||
|
||||
string blockContent = vdfContent.Substring(blockStart, blockEnd - blockStart);
|
||||
|
||||
var keyValuePattern = new Regex("\"(\\d+)\"\\s*\\{[^}]*\"name\"\\s*\"([^\"]+)\"",
|
||||
RegexOptions.Multiline);
|
||||
|
||||
var matches = keyValuePattern.Matches(blockContent);
|
||||
foreach (Match match in matches)
|
||||
{
|
||||
string key = match.Groups[1].Value;
|
||||
string name = match.Groups[2].Value;
|
||||
|
||||
if (key != "0")
|
||||
{
|
||||
compatToolMapping[key] = name;
|
||||
}
|
||||
}
|
||||
|
||||
return compatToolMapping;
|
||||
}
|
||||
|
||||
private static string GetSteamVdfCompatTool()
|
||||
{
|
||||
string steamPath = _steamPath;
|
||||
string configVdfPath = Path.Combine(steamPath, "config", "config.vdf");
|
||||
if (!File.Exists(configVdfPath))
|
||||
{
|
||||
logger.Error("config.vdf not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
string vdfContent = File.ReadAllText(configVdfPath);
|
||||
var compatToolMapping = ExtractCompatToolMapping(vdfContent);
|
||||
|
||||
if (compatToolMapping.TryGetValue("438100", out string name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string ParseWineRegOutput(string output, string keyName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(output))
|
||||
return null;
|
||||
|
||||
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(line =>
|
||||
!string.IsNullOrWhiteSpace(line) &&
|
||||
!line.Contains("fixme:") &&
|
||||
!line.Contains("wine:"))
|
||||
.ToArray();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var parts = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => p.Trim())
|
||||
.ToArray();
|
||||
if (parts.Length >= 3 && parts[0].Contains(keyName))
|
||||
{
|
||||
var valueType = parts[parts.Length - 2];
|
||||
var value = parts[parts.Length - 1];
|
||||
|
||||
switch (valueType)
|
||||
{
|
||||
case "REG_BINARY":
|
||||
try
|
||||
{
|
||||
// Treat the value as a plain hex string and decode it to ASCII
|
||||
var hexValues = Enumerable.Range(0, value.Length / 2)
|
||||
.Select(i => value.Substring(i * 2, 2)) // Break string into chunks of 2
|
||||
.Select(hex => Convert.ToByte(hex, 16)) // Convert each chunk to a byte
|
||||
.ToArray();
|
||||
|
||||
return Encoding.ASCII.GetString(hexValues).TrimEnd('\0');
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Error parsing REG_BINARY as plain hex string: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
|
||||
case "REG_DWORD":
|
||||
return "REG_DWORD";
|
||||
|
||||
default:
|
||||
logger.Error($"Unsupported parsed registry value type: {valueType}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string ParseWineRegOutputEx(string output, string keyName)
|
||||
{
|
||||
var lines = output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
string currentKey = null;
|
||||
string currentValue = null;
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i].Trim();
|
||||
if (line.Contains("="))
|
||||
{
|
||||
var parts = line.Split(new[] { '=' }, 2);
|
||||
currentKey = parts[0].Trim();
|
||||
currentValue = parts[1].Trim();
|
||||
|
||||
string escapedString = @$"{currentValue}";
|
||||
escapedString = escapedString.Replace("\\", "");
|
||||
currentValue = escapedString;
|
||||
|
||||
if (currentKey.Contains(keyName))
|
||||
{
|
||||
if (currentValue.EndsWith(",\\"))
|
||||
{
|
||||
var multiLineValue = new StringBuilder(currentValue.TrimEnd('\\'));
|
||||
while (currentValue.EndsWith(",\\"))
|
||||
{
|
||||
currentValue = lines[++i].Trim();
|
||||
multiLineValue.Append(currentValue.TrimEnd('\\'));
|
||||
}
|
||||
currentValue = multiLineValue.ToString();
|
||||
}
|
||||
|
||||
if (currentValue.StartsWith("dword:"))
|
||||
{
|
||||
return int.Parse(currentValue.Substring(6), System.Globalization.NumberStyles.HexNumber).ToString();
|
||||
}
|
||||
else if (currentValue.StartsWith("hex:"))
|
||||
{
|
||||
var hexValues = currentValue.Substring(4).Replace("\\", "").Split(',');
|
||||
var bytes = hexValues.Select(hex => Convert.ToByte(hex, 16)).ToArray();
|
||||
var decodedString = Encoding.UTF8.GetString(bytes);
|
||||
|
||||
if (decodedString.StartsWith("[") && decodedString.EndsWith("]"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonObject = Newtonsoft.Json.JsonConvert.DeserializeObject(decodedString);
|
||||
return Newtonsoft.Json.JsonConvert.SerializeObject(jsonObject, Newtonsoft.Json.Formatting.Indented);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Error parsing JSON: {ex.Message}");
|
||||
return decodedString;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return currentValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Error($"Key not found: {keyName}");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetVRChatWinePath()
|
||||
{
|
||||
string compatTool = GetSteamVdfCompatTool();
|
||||
if (compatTool == null)
|
||||
{
|
||||
logger.Error("CompatTool not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
string steamPath = _steamPath;
|
||||
string steamAppsCommonPath = Path.Combine(steamPath, "steamapps", "common");
|
||||
string compatabilityToolsPath = Path.Combine(steamPath, "compatibilitytools.d");
|
||||
string protonPath = Path.Combine(steamAppsCommonPath, compatTool);
|
||||
string compatToolPath = Path.Combine(compatabilityToolsPath, compatTool);
|
||||
string winePath = "";
|
||||
if (Directory.Exists(compatToolPath))
|
||||
{
|
||||
winePath = Path.Combine(compatToolPath, "files", "bin", "wine");
|
||||
if (!File.Exists(winePath))
|
||||
{
|
||||
Console.WriteLine("Wine not found in CompatTool path");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (Directory.Exists(protonPath))
|
||||
{
|
||||
winePath = Path.Combine(protonPath, "dist", "bin", "wine");
|
||||
if (!File.Exists(winePath))
|
||||
{
|
||||
logger.Error("Wine not found in Proton path");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (Directory.Exists(compatabilityToolsPath))
|
||||
{
|
||||
var dirs = Directory.GetDirectories(compatabilityToolsPath);
|
||||
foreach (var dir in dirs)
|
||||
{
|
||||
if (dir.Contains(compatTool))
|
||||
{
|
||||
winePath = Path.Combine(dir, "files", "bin", "wine");
|
||||
if (File.Exists(winePath))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!File.Exists(winePath))
|
||||
{
|
||||
Console.WriteLine("Wine not found in CompatTool path");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (winePath == "")
|
||||
{
|
||||
logger.Error("CompatTool and Proton not found");
|
||||
return null;
|
||||
}
|
||||
|
||||
return winePath;
|
||||
}
|
||||
|
||||
private ProcessStartInfo GetWineProcessStartInfo(string winePath, string winePrefix, string wineCommand)
|
||||
{
|
||||
var processStartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "/bin/bash",
|
||||
Arguments = $"-c \"{wineCommand.Replace("\"", "\\\"")}\"",
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
};
|
||||
processStartInfo.Environment["WINEFSYNC"] = "1";
|
||||
processStartInfo.Environment["WINEPREFIX"] = winePrefix;
|
||||
//processStartInfo.Environment["WINEDEBUG"] = "-all";
|
||||
|
||||
return processStartInfo;
|
||||
}
|
||||
|
||||
private string GetWineRegCommand(string command)
|
||||
{
|
||||
string winePath = GetVRChatWinePath();
|
||||
string winePrefix = _vrcPrefixPath;
|
||||
string wineRegCommand = $"\"{winePath}\" reg {command}";
|
||||
ProcessStartInfo processStartInfo = GetWineProcessStartInfo(winePath, winePrefix, wineRegCommand);
|
||||
using var process = Process.Start(processStartInfo);
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
string error = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
if (!string.IsNullOrEmpty(error) &&
|
||||
!error.Contains("wineserver: using server-side synchronization.") &&
|
||||
!error.Contains("fixme:wineusb:query_id"))
|
||||
{
|
||||
logger.Error($"Wine reg command error: {error}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private string GetWineRegCommandEx(string regCommand)
|
||||
{
|
||||
string winePrefix = _vrcPrefixPath;
|
||||
string filePath = Path.Combine(winePrefix, "user.reg");
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"Registry file not found at {filePath}");
|
||||
|
||||
var match = Regex.Match(regCommand, @"^(add|query|delete)\s+""([^""]+)""(?:\s+/v\s+""([^""]+)"")?(?:\s+/t\s+(\w+))?(?:\s+/d\s+([^\s]+))?.*$");
|
||||
if (!match.Success)
|
||||
throw new ArgumentException("Invalid command format.");
|
||||
|
||||
string action = match.Groups[1].Value.ToLower();
|
||||
string valueName = match.Groups[3].Success ? match.Groups[3].Value : null;
|
||||
string valueType = match.Groups[4].Success ? match.Groups[4].Value : null;
|
||||
string valueData = match.Groups[5].Success ? match.Groups[5].Value : null;
|
||||
|
||||
var lines = File.ReadAllLines(filePath).ToList();
|
||||
var updatedLines = new List<string>();
|
||||
bool keyFound = false;
|
||||
bool valueFound = false;
|
||||
bool inVRChatSection = false;
|
||||
int headerEndIndex = -1;
|
||||
string keyHeader = "[Software\\\\VRChat\\\\VRChat]";
|
||||
|
||||
if (action == "add")
|
||||
{
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
string line = lines[i].Trim();
|
||||
|
||||
if (line.StartsWith(keyHeader))
|
||||
{
|
||||
inVRChatSection = true;
|
||||
keyFound = true;
|
||||
headerEndIndex = i;
|
||||
|
||||
// Add header and metadata lines
|
||||
while (i < lines.Count && (lines[i].StartsWith("#") || lines[i].StartsWith("@") || lines[i].Trim().StartsWith(keyHeader)))
|
||||
{
|
||||
updatedLines.Add(lines[i]);
|
||||
i++;
|
||||
}
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
else if (inVRChatSection && line.StartsWith("["))
|
||||
{
|
||||
inVRChatSection = false;
|
||||
}
|
||||
|
||||
if (inVRChatSection && valueName != null)
|
||||
{
|
||||
if (line.TrimStart().StartsWith($"\"{valueName}\"="))
|
||||
{
|
||||
valueFound = true;
|
||||
updatedLines.Add($"\"{valueName}\"={GetRegistryValueFormat(valueType, valueData)}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
updatedLines.Add(lines[i]);
|
||||
}
|
||||
|
||||
// Add new value if not found but section exists
|
||||
if (keyFound && !valueFound && valueName != null)
|
||||
{
|
||||
var insertIndex = headerEndIndex + 2;
|
||||
while (insertIndex < updatedLines.Count &&
|
||||
(updatedLines[insertIndex].StartsWith("#") || updatedLines[insertIndex].StartsWith("@")))
|
||||
{
|
||||
insertIndex++;
|
||||
}
|
||||
updatedLines.Insert(insertIndex, $"\"{valueName}\"={GetRegistryValueFormat(valueType, valueData)}");
|
||||
}
|
||||
|
||||
File.WriteAllLines(filePath, updatedLines);
|
||||
return $"Command '{regCommand}' executed successfully.";
|
||||
}
|
||||
else if (action == "query")
|
||||
{
|
||||
if (!valueName.Contains("_h"))
|
||||
{
|
||||
valueName = AddHashToKeyName(valueName);
|
||||
}
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (line.Contains(valueName))
|
||||
{
|
||||
return line;
|
||||
}
|
||||
}
|
||||
|
||||
return $"Value \"{valueName}\" not found.";
|
||||
}
|
||||
|
||||
logger.Error($"Unsupported registry command: {regCommand}");
|
||||
|
||||
return $"Command '{regCommand}' executed successfully.";
|
||||
}
|
||||
|
||||
private static string GetRegistryValueFormat(string valueType, string valueData)
|
||||
{
|
||||
if (valueType?.ToUpper() == "REG_DWORD100")
|
||||
{
|
||||
double inputValue = double.Parse(valueData);
|
||||
Span<byte> dataBytes = stackalloc byte[sizeof(double)];
|
||||
BitConverter.TryWriteBytes(dataBytes, inputValue);
|
||||
var hexValues = dataBytes.ToArray().Select(b => b.ToString("X2")).ToArray();
|
||||
var byteString = string.Join(",", hexValues).ToLower();
|
||||
var result = $"hex(4):{byteString}";
|
||||
return result;
|
||||
}
|
||||
|
||||
return valueType?.ToUpper() switch
|
||||
{
|
||||
"REG_DWORD" => $"dword:{int.Parse(valueData):X8}",
|
||||
_ => throw new ArgumentException($"Unsupported registry value type: {valueType}"),
|
||||
};
|
||||
}
|
||||
|
||||
public override object GetVRChatRegistryKey(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
key = AddHashToKeyName(key);
|
||||
string regCommand = $"query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"{key}\"";
|
||||
var queryResult = GetWineRegCommand(regCommand);
|
||||
if (queryResult == null)
|
||||
return null;
|
||||
|
||||
var result = ParseWineRegOutput(queryResult, key);
|
||||
if (result == "REG_DWORD")
|
||||
{
|
||||
queryResult = GetWineRegCommandEx(regCommand);
|
||||
result = ParseWineRegOutputEx(queryResult, key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Exception in GetRegistryValueFromWine: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetVRChatRegistryKeyString(string key)
|
||||
{
|
||||
// for electron
|
||||
return GetVRChatRegistryKey(key)?.ToString();
|
||||
}
|
||||
|
||||
// TODO: check this
|
||||
public async Task SetVRChatRegistryKeyAsync(string key, object value, int typeInt)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
SetVRChatRegistryKey(key, value, typeInt);
|
||||
});
|
||||
}
|
||||
|
||||
public override bool SetVRChatRegistryKey(string key, object value, int typeInt)
|
||||
{
|
||||
var type = (RegistryValueKind)typeInt;
|
||||
var keyName = AddHashToKeyName(key);
|
||||
switch (type)
|
||||
{
|
||||
case RegistryValueKind.Binary:
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
|
||||
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
byte[] byteArray = Encoding.UTF8.GetBytes(jsonElement.GetString());
|
||||
var data = BitConverter.ToString(byteArray).Replace("-", "");
|
||||
if (data.Length == 0)
|
||||
data = "\"\"";
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + data + " /f";
|
||||
var addResult = GetWineRegCommand(regCommand);
|
||||
if (addResult == null)
|
||||
return false;
|
||||
}
|
||||
else if (jsonElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
byte[] byteArray = jsonElement.EnumerateArray()
|
||||
.Select(e => (byte)e.GetInt32()) // Convert each element to byte
|
||||
.ToArray();
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + BitConverter.ToString(byteArray).Replace("-", "") + " /f";
|
||||
var addResult = GetWineRegCommand(regCommand);
|
||||
if (addResult == null)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Invalid value for REG_BINARY: {value}. It must be a JSON string or array.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (value is string jsonArray)
|
||||
{
|
||||
byte[] byteArray = Encoding.UTF8.GetBytes(jsonArray);
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + BitConverter.ToString(byteArray).Replace("-", "") + " /f";
|
||||
var addResult = GetWineRegCommand(regCommand);
|
||||
if (addResult == null)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Invalid value for REG_BINARY: {value}. It must be a JsonElement.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case RegistryValueKind.DWord:
|
||||
if (value is int intValue)
|
||||
{
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + intValue + " /f";
|
||||
var addResult = GetWineRegCommandEx(regCommand);
|
||||
if (addResult == null)
|
||||
return false;
|
||||
}
|
||||
else if (value is string stringValue && int.TryParse(stringValue, out int parsedIntValue))
|
||||
{
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + parsedIntValue + " /f";
|
||||
var addResult = GetWineRegCommandEx(regCommand);
|
||||
if (addResult == null)
|
||||
return false;
|
||||
}
|
||||
else if (value is JsonElement jsonElementValue && jsonElementValue.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
int parsedInt32Value = jsonElementValue.GetInt32();
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + parsedInt32Value + " /f";
|
||||
var addResult = GetWineRegCommandEx(regCommand);
|
||||
if (addResult == null)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"Invalid value for REG_DWORD: {value}. It must be a valid integer.");
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.Error($"Unsupported set registry value type: {typeInt}");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public override void SetVRChatRegistryKey(string key, byte[] value)
|
||||
{
|
||||
var keyName = AddHashToKeyName(key);
|
||||
var data = BitConverter.ToString(value).Replace("-", "");
|
||||
if (data.Length == 0)
|
||||
data = "\"\"";
|
||||
var regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_BINARY /d " + data + " /f";
|
||||
GetWineRegCommand(regCommand);
|
||||
}
|
||||
|
||||
public override Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: no object type
|
||||
// public Dictionary<string, Dictionary<string, object>> GetVRChatRegistry()
|
||||
public string GetVRChatRegistryJson()
|
||||
{
|
||||
var registry = new Dictionary<string, Dictionary<string, object>>();
|
||||
string regCommand = "query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\"";
|
||||
var queryResult = GetWineRegCommand(regCommand);
|
||||
if (queryResult == null)
|
||||
return null;
|
||||
|
||||
var lines = queryResult.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Where(line =>
|
||||
!string.IsNullOrWhiteSpace(line) &&
|
||||
!line.Contains("fixme:") &&
|
||||
!line.Contains("wine:"))
|
||||
.ToArray();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var parts = line.Split(new[] { '\t', ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => p.Trim())
|
||||
.ToArray();
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
var keyName = parts[0];
|
||||
var index = keyName.LastIndexOf("_h", StringComparison.Ordinal);
|
||||
if (index > 0)
|
||||
keyName = keyName.Substring(0, index);
|
||||
var valueType = parts[parts.Length - 2];
|
||||
var value = parts[parts.Length - 1];
|
||||
|
||||
switch (valueType)
|
||||
{
|
||||
case "REG_BINARY":
|
||||
try
|
||||
{
|
||||
// Treat the value as a plain hex string and decode it to ASCII
|
||||
var hexValues = Enumerable.Range(0, value.Length / 2)
|
||||
.Select(i => value.Substring(i * 2, 2)) // Break string into chunks of 2
|
||||
.Select(hex => Convert.ToByte(hex, 16)) // Convert each chunk to a byte
|
||||
.ToArray();
|
||||
|
||||
var binDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", Encoding.ASCII.GetString(hexValues).TrimEnd('\0') },
|
||||
{ "type", 3 }
|
||||
};
|
||||
registry.Add(keyName, binDict);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Error parsing REG_BINARY as plain hex string: {ex.Message}");
|
||||
}
|
||||
break;
|
||||
|
||||
case "REG_DWORD":
|
||||
string regCommandExDword = $"query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"{keyName}\"";
|
||||
var queryResultExDword = GetWineRegCommandEx(regCommandExDword);
|
||||
if (queryResultExDword == null)
|
||||
break;
|
||||
|
||||
var resultExDword = ParseWineRegOutputEx(queryResultExDword, keyName);
|
||||
if (resultExDword == null)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
if (resultExDword.StartsWith("hex(4)"))
|
||||
{
|
||||
string hexString = resultExDword;
|
||||
string[] hexValues = hexString.Split(':')[1].Split(',');
|
||||
byte[] byteValues = hexValues.Select(h => Convert.ToByte(h, 16)).ToArray();
|
||||
if (byteValues.Length != 8)
|
||||
{
|
||||
throw new ArgumentException("Input does not represent a valid 8-byte double-precision float.");
|
||||
}
|
||||
double parsedDouble = BitConverter.ToDouble(byteValues, 0);
|
||||
var doubleDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", parsedDouble },
|
||||
{ "type", 100 } // it's special
|
||||
};
|
||||
registry.Add(keyName, doubleDict);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert dword value to integer
|
||||
int parsedInt = int.Parse(resultExDword);
|
||||
var dwordDict = new Dictionary<string, object>
|
||||
{
|
||||
{ "data", parsedInt },
|
||||
{ "type", 4 }
|
||||
};
|
||||
registry.Add(keyName, dwordDict);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Error parsing REG_DWORD: {ex.Message}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Newtonsoft.Json.JsonConvert.SerializeObject(registry, Newtonsoft.Json.Formatting.Indented);
|
||||
}
|
||||
|
||||
public override void SetVRChatRegistry(string json)
|
||||
{
|
||||
var dict = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, object>>>(json);
|
||||
foreach (var item in dict)
|
||||
{
|
||||
var data = (JsonElement)item.Value["data"];
|
||||
if (!int.TryParse(item.Value["type"].ToString(), out var type))
|
||||
throw new Exception("Unknown type: " + item.Value["type"]);
|
||||
|
||||
string keyName = AddHashToKeyName(item.Key);
|
||||
if (type == 4)
|
||||
{
|
||||
int intValue = data.GetInt32();
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD /d " + intValue + " /f";
|
||||
var addResult = GetWineRegCommandEx(regCommand);
|
||||
if (addResult == null)
|
||||
continue;
|
||||
}
|
||||
else if (type == 100)
|
||||
{
|
||||
var valueLong = data.GetDouble();
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /v \"" + keyName + "\" /t REG_DWORD100 /d " + valueLong + " /f";
|
||||
var addResult = GetWineRegCommandEx(regCommand);
|
||||
if (addResult == null)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This slows down the recovery process but using async can be problematic
|
||||
if (data.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (int.TryParse(data.ToString(), out var intValue))
|
||||
{
|
||||
SetVRChatRegistryKey(item.Key, intValue, type);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Exception("Unknown number type: " + item.Key);
|
||||
}
|
||||
|
||||
SetVRChatRegistryKey(item.Key, data, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool HasVRChatRegistryFolder()
|
||||
{
|
||||
string regCommand = "query \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\"";
|
||||
var queryResult = GetWineRegCommand(regCommand);
|
||||
if (queryResult == null)
|
||||
return false;
|
||||
|
||||
return !string.IsNullOrEmpty(queryResult);
|
||||
}
|
||||
|
||||
private void CreateVRChatRegistryFolder()
|
||||
{
|
||||
string regCommand = "add \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /f";
|
||||
GetWineRegCommand(regCommand);
|
||||
}
|
||||
|
||||
public override void DeleteVRChatRegistryFolder()
|
||||
{
|
||||
string regCommand = "delete \"HKEY_CURRENT_USER\\SOFTWARE\\VRChat\\VRChat\" /f";
|
||||
GetWineRegCommand(regCommand);
|
||||
}
|
||||
|
||||
public override string ReadVrcRegJsonFile(string filepath)
|
||||
{
|
||||
if (!File.Exists(filepath))
|
||||
return string.Empty;
|
||||
|
||||
var json = File.ReadAllText(filepath);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Dotnet/AppApi/Electron/Screenshot.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApiElectron
|
||||
{
|
||||
public override string AddScreenshotMetadata(string path, string metadataString, string worldId, bool changeFilename = false)
|
||||
{
|
||||
var winePrefix = Path.Combine(_vrcPrefixPath, "/drive_c/");
|
||||
var winePath = path.Substring(3).Replace("\\", "/");
|
||||
path = Path.Combine(winePrefix, winePath);
|
||||
|
||||
var fileName = Path.GetFileNameWithoutExtension(path);
|
||||
if (!File.Exists(path) || !path.EndsWith(".png") || !fileName.StartsWith("VRChat_"))
|
||||
return string.Empty;
|
||||
|
||||
if (changeFilename)
|
||||
{
|
||||
var newFileName = $"{fileName}_{worldId}";
|
||||
var newPath = Path.Combine(Path.GetDirectoryName(path), newFileName + Path.GetExtension(path));
|
||||
File.Move(path, newPath);
|
||||
path = newPath;
|
||||
}
|
||||
|
||||
ScreenshotHelper.WritePNGDescription(path, metadataString);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using CefSharp;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
private void OnProcessStateChanged(MonitoredProcess monitoredProcess)
|
||||
{
|
||||
if (!monitoredProcess.HasName("VRChat") && !monitoredProcess.HasName("vrserver"))
|
||||
return;
|
||||
|
||||
CheckGameRunning();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the VRChat game and SteamVR are currently running and updates the browser's JavaScript function $app.updateIsGameRunning with the results.
|
||||
/// </summary>
|
||||
public void CheckGameRunning()
|
||||
{
|
||||
var isGameRunning = false;
|
||||
var isSteamVRRunning = false;
|
||||
var isHmdAfk = false;
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("VRChat"))
|
||||
isGameRunning = true;
|
||||
|
||||
if (Wine.GetIfWine())
|
||||
{
|
||||
var wineTmpPath = Path.Combine(Program.AppDataDirectory, "wine.tmp");
|
||||
if (File.Exists(wineTmpPath))
|
||||
{
|
||||
var wineTmp = File.ReadAllText(wineTmpPath);
|
||||
if (wineTmp.Contains("isGameRunning=true"))
|
||||
isGameRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ProcessMonitor.Instance.IsProcessRunning("vrserver"))
|
||||
isSteamVRRunning = true;
|
||||
|
||||
if (Program.VRCXVRInstance != null)
|
||||
isHmdAfk = Program.VRCXVRInstance.IsHmdAfk;
|
||||
|
||||
// TODO: fix this throwing an exception for being called before the browser is ready. somehow it gets past the checks
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.updateIsGameRunning", isGameRunning, isSteamVRRunning, isHmdAfk);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the VRChat process if it is currently running.
|
||||
/// </summary>
|
||||
/// <returns>The number of processes that were killed (0 or 1).</returns>
|
||||
public int QuitGame()
|
||||
{
|
||||
var processes = Process.GetProcessesByName("vrchat");
|
||||
if (processes.Length == 1)
|
||||
processes[0].Kill();
|
||||
|
||||
return processes.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kills the install.exe process after exiting game.
|
||||
/// </summary>
|
||||
/// <returns>Whether the process is killed (true or false).</returns>
|
||||
public bool KillInstall()
|
||||
{
|
||||
bool isSuccess = false;
|
||||
var processes = Process.GetProcessesByName("install");
|
||||
foreach (var p in processes)
|
||||
{
|
||||
// "E:\SteamLibrary\steamapps\common\VRChat\install.exe"
|
||||
var match = Regex.Match(GetProcessName(p.Id), "(.+?\\\\VRChat.*)(!?\\\\install.exe)");
|
||||
if (match.Success)
|
||||
{
|
||||
// Sometimes install.exe is suspended
|
||||
ResumeProcess(p.Id);
|
||||
p.Kill();
|
||||
isSuccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isSuccess;
|
||||
}
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
private static extern uint NtResumeProcess([In] IntPtr processHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
|
||||
private static extern bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, [Out, MarshalAs(UnmanagedType.LPTStr)] StringBuilder lpExeName, ref uint lpdwSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(uint processAccess, bool inheritHandle, int processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle([In] IntPtr handle);
|
||||
|
||||
public static void ResumeProcess(int processId)
|
||||
{
|
||||
IntPtr hProc = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
// Gets the handle to the Process
|
||||
// 0x800 mean required to suspend or resume a process.
|
||||
hProc = OpenProcess(0x800, false, processId);
|
||||
if (hProc != IntPtr.Zero)
|
||||
NtResumeProcess(hProc);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// close handle.
|
||||
if (hProc != IntPtr.Zero)
|
||||
CloseHandle(hProc);
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetProcessName(int pid)
|
||||
{
|
||||
IntPtr hProc = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
// 0x400 mean required to retrieve certain information about a process, such as its token, exit code, and priority class.
|
||||
// 0x10 mean required to read memory in a process using ReadProcessMemory.
|
||||
hProc = OpenProcess(0x400 | 0x10, false, pid);
|
||||
if (hProc != IntPtr.Zero)
|
||||
{
|
||||
int lengthSb = 4000;
|
||||
uint lpSize = 65535;
|
||||
var sb = new StringBuilder(lengthSb);
|
||||
string result = String.Empty;
|
||||
if (QueryFullProcessImageName(hProc, 0, sb, ref lpSize))
|
||||
{
|
||||
result = sb.ToString();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (hProc != IntPtr.Zero)
|
||||
CloseHandle(hProc);
|
||||
}
|
||||
return String.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the VRChat game process with the specified command-line arguments.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The command-line arguments to pass to the VRChat game.</param>
|
||||
public bool StartGame(string arguments)
|
||||
{
|
||||
// try stream first
|
||||
try
|
||||
{
|
||||
using var key = Registry.ClassesRoot.OpenSubKey(@"steam\shell\open\command");
|
||||
// "C:\Program Files (x86)\Steam\steam.exe" -- "%1"
|
||||
var match = Regex.Match(key.GetValue(string.Empty) as string, "^\"(.+?)\\\\steam.exe\"");
|
||||
if (match.Success)
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
// var _arguments = Uri.EscapeDataString(arguments);
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = path,
|
||||
FileName = $"{path}\\steam.exe",
|
||||
UseShellExecute = false,
|
||||
Arguments = $"-applaunch 438100 {arguments}"
|
||||
})
|
||||
?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Warn("Failed to start VRChat from Steam");
|
||||
}
|
||||
|
||||
// fallback
|
||||
try
|
||||
{
|
||||
using var key = Registry.ClassesRoot.OpenSubKey(@"VRChat\shell\open\command");
|
||||
// "C:\Program Files (x86)\Steam\steamapps\common\VRChat\launch.exe" "%1" %*
|
||||
var match = Regex.Match(key.GetValue(string.Empty) as string, "(?!\")(.+?\\\\VRChat.*)(!?\\\\launch.exe\")");
|
||||
if (match.Success)
|
||||
{
|
||||
var path = match.Groups[1].Value;
|
||||
return StartGameFromPath(path, arguments);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
logger.Warn("Failed to start VRChat from registry");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the VRChat game process with the specified command-line arguments from the given path.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the VRChat game executable.</param>
|
||||
/// <param name="arguments">The command-line arguments to pass to the VRChat game.</param>
|
||||
/// <returns>True if the game was started successfully, false otherwise.</returns>
|
||||
public bool StartGameFromPath(string path, string arguments)
|
||||
{
|
||||
if (!path.EndsWith(".exe"))
|
||||
path = Path.Combine(path, "launch.exe");
|
||||
|
||||
if (!path.EndsWith("launch.exe") || !File.Exists(path))
|
||||
return false;
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
WorkingDirectory = Path.GetDirectoryName(path),
|
||||
FileName = path,
|
||||
UseShellExecute = false,
|
||||
Arguments = arguments
|
||||
})?.Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a dictionary of moderations for the specified user from the VRChat LocalPlayerModerations folder.
|
||||
/// </summary>
|
||||
/// <param name="currentUserId">The ID of the current user.</param>
|
||||
/// <returns>A dictionary of moderations for the specified user, or null if the file does not exist.</returns>
|
||||
public Dictionary<string, short> GetVRChatModerations(string currentUserId)
|
||||
{
|
||||
// 004 = hideAvatar
|
||||
// 005 = showAvatar
|
||||
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
||||
if (!File.Exists(filePath))
|
||||
return null;
|
||||
|
||||
var output = new Dictionary<string, short>();
|
||||
using (var reader = new StreamReader(filePath))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var index = line.IndexOf(' ');
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
var userId = line.Substring(0, index);
|
||||
var type = short.Parse(line.Substring(line.Length - 3));
|
||||
output.Add(userId, type);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the moderation type for the specified user from the VRChat LocalPlayerModerations folder.
|
||||
/// </summary>
|
||||
/// <param name="currentUserId">The ID of the current user.</param>
|
||||
/// <param name="userId">The ID of the user to retrieve the moderation type for.</param>
|
||||
/// <returns>The moderation type for the specified user, or 0 if the file does not exist or the user is not found.</returns>
|
||||
public short GetVRChatUserModeration(string currentUserId, string userId)
|
||||
{
|
||||
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
||||
if (!File.Exists(filePath))
|
||||
return 0;
|
||||
|
||||
using (var reader = new StreamReader(filePath))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
var index = line.IndexOf(' ');
|
||||
if (index <= 0)
|
||||
continue;
|
||||
|
||||
if (userId == line.Substring(0, index))
|
||||
{
|
||||
return short.Parse(line.Substring(line.Length - 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the moderation type for the specified user in the VRChat LocalPlayerModerations folder.
|
||||
/// </summary>
|
||||
/// <param name="currentUserId">The ID of the current user.</param>
|
||||
/// <param name="userId">The ID of the user to set the moderation type for.</param>
|
||||
/// <param name="type">The moderation type to set for the specified user.</param>
|
||||
/// <returns>True if the operation was successful, false otherwise.</returns>
|
||||
public bool SetVRChatUserModeration(string currentUserId, string userId, int type)
|
||||
{
|
||||
var filePath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $@"Low\VRChat\VRChat\LocalPlayerModerations\{currentUserId}-show-hide-user.vrcset";
|
||||
if (!File.Exists(filePath))
|
||||
return false;
|
||||
|
||||
var lines = File.ReadAllLines(filePath).ToList();
|
||||
var index = lines.FindIndex(x => x.StartsWith(userId));
|
||||
if (index >= 0)
|
||||
lines.RemoveAt(index);
|
||||
|
||||
if (type != 0)
|
||||
{
|
||||
var sb = new StringBuilder(userId);
|
||||
while (sb.Length < 64)
|
||||
sb.Append(' ');
|
||||
|
||||
sb.Append(type.ToString("000"));
|
||||
lines.Add(sb.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(filePath, lines);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public partial class AppApi
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads the VRChat config file and returns its contents as a string.
|
||||
/// </summary>
|
||||
/// <returns>The contents of the VRChat config file as a string, or an empty string if the file does not exist.</returns>
|
||||
public string ReadConfigFile()
|
||||
{
|
||||
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
|
||||
var configFile = Path.Combine(logPath, "config.json");
|
||||
if (!Directory.Exists(logPath) || !File.Exists(configFile))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(configFile);
|
||||
return json;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the specified JSON string to the VRChat config file.
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string to write to the config file.</param>
|
||||
public void WriteConfigFile(string json)
|
||||
{
|
||||
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat\";
|
||||
var configFile = Path.Combine(logPath, "config.json");
|
||||
File.WriteAllText(configFile, json);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,30 +11,21 @@ using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class AssetBundleCacher
|
||||
public class AssetBundleManager
|
||||
{
|
||||
public static readonly AssetBundleCacher Instance;
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
public static readonly AssetBundleManager Instance;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static AssetBundleCacher()
|
||||
static AssetBundleManager()
|
||||
{
|
||||
Instance = new AssetBundleCacher();
|
||||
Instance = new AssetBundleManager();
|
||||
}
|
||||
|
||||
public static string DownloadTempLocation;
|
||||
public static string DownloadDestinationLocation;
|
||||
public static string DownloadHashLocation;
|
||||
public static int DownloadProgress;
|
||||
public static int DownloadSize;
|
||||
public static bool DownloadCanceled;
|
||||
public static WebClient client;
|
||||
public static Process process;
|
||||
|
||||
public string GetAssetId(string id, string variant = "")
|
||||
{
|
||||
using(var sha256 = SHA256.Create())
|
||||
@@ -96,7 +87,7 @@ namespace VRCX
|
||||
|
||||
public string GetVRChatCacheLocation()
|
||||
{
|
||||
return AppApi.Instance.GetVRChatCacheLocation();
|
||||
return Program.AppApiInstance.GetVRChatCacheLocation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,103 +145,6 @@ namespace VRCX
|
||||
return new Tuple<long, bool, string>(fileSize, isLocked, cachePath);
|
||||
}
|
||||
|
||||
// old asset bundle cacher downloader method reused for updating, it's not pretty
|
||||
public void DownloadFile(string fileUrl, string hashUrl, int size)
|
||||
{
|
||||
#pragma warning disable SYSLIB0014 // Type or member is obsolete
|
||||
client = new WebClient();
|
||||
#pragma warning restore SYSLIB0014 // Type or member is obsolete
|
||||
client.Headers.Add("user-agent", Program.Version);
|
||||
if (WebApi.ProxySet)
|
||||
client.Proxy = WebApi.Proxy;
|
||||
DownloadProgress = 0;
|
||||
DownloadSize = size;
|
||||
DownloadCanceled = false;
|
||||
DownloadTempLocation = Path.Combine(Program.AppDataDirectory, "tempDownload.exe");
|
||||
DownloadDestinationLocation = Path.Combine(Program.AppDataDirectory, "update.exe");
|
||||
DownloadHashLocation = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
|
||||
if (File.Exists(DownloadHashLocation))
|
||||
File.Delete(DownloadHashLocation);
|
||||
if (!string.IsNullOrEmpty(hashUrl))
|
||||
client.DownloadFile(new Uri(hashUrl), DownloadHashLocation);
|
||||
|
||||
client.DownloadProgressChanged += new DownloadProgressChangedEventHandler(DownloadProgressCallback);
|
||||
client.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompletedCallback);
|
||||
client.DownloadFileAsync(new Uri(fileUrl), DownloadTempLocation);
|
||||
}
|
||||
|
||||
public void CancelDownload()
|
||||
{
|
||||
DownloadCanceled = true;
|
||||
try
|
||||
{
|
||||
client?.CancelAsync();
|
||||
if (File.Exists(DownloadTempLocation))
|
||||
File.Delete(DownloadTempLocation);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
DownloadProgress = -4;
|
||||
}
|
||||
|
||||
public int CheckDownloadProgress()
|
||||
{
|
||||
return DownloadProgress;
|
||||
}
|
||||
|
||||
private static void DownloadProgressCallback(object sender, DownloadProgressChangedEventArgs e)
|
||||
{
|
||||
DownloadProgress = e.ProgressPercentage;
|
||||
}
|
||||
|
||||
private static void DownloadCompletedCallback(object sender, AsyncCompletedEventArgs e)
|
||||
{
|
||||
if (DownloadCanceled)
|
||||
{
|
||||
if (File.Exists(DownloadTempLocation))
|
||||
File.Delete(DownloadTempLocation);
|
||||
return;
|
||||
}
|
||||
if (!File.Exists(DownloadTempLocation))
|
||||
{
|
||||
DownloadProgress = -15;
|
||||
return;
|
||||
}
|
||||
FileInfo data = new FileInfo(DownloadTempLocation);
|
||||
if (data.Length != DownloadSize)
|
||||
{
|
||||
File.Delete(DownloadTempLocation);
|
||||
DownloadProgress = -15;
|
||||
return;
|
||||
}
|
||||
if (File.Exists(DownloadHashLocation))
|
||||
{
|
||||
logger.Info("Updater: Checking hash");
|
||||
var lines = File.ReadAllLines(DownloadHashLocation);
|
||||
var hash = lines.Length > 0 ? lines[0].Split(' ') : new[] { "" };
|
||||
using (var sha256 = SHA256.Create())
|
||||
using (var stream = File.OpenRead(DownloadTempLocation))
|
||||
{
|
||||
var hashBytes = sha256.ComputeHash(stream);
|
||||
var hashString = BitConverter.ToString(hashBytes).Replace("-", "");
|
||||
if (!hashString.Equals(hash[0], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.Error($"Updater: Hash check failed file:{hashString} remote:{hash[0]}");
|
||||
// can't delete file yet because it's in use
|
||||
DownloadProgress = -14;
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger.Info("Updater: Hash check passed");
|
||||
}
|
||||
|
||||
if (File.Exists(DownloadDestinationLocation))
|
||||
File.Delete(DownloadDestinationLocation);
|
||||
File.Move(DownloadTempLocation, DownloadDestinationLocation);
|
||||
DownloadProgress = -16;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the cache directory for a specific asset bundle.
|
||||
/// </summary>
|
||||
@@ -129,7 +129,7 @@ namespace VRCX
|
||||
/// <summary>
|
||||
/// Kills all running child processes.
|
||||
/// </summary>
|
||||
internal void KillChildProcesses()
|
||||
private void KillChildProcesses()
|
||||
{
|
||||
UpdateChildProcesses(); // Ensure the list contains all current child processes.
|
||||
|
||||
@@ -240,7 +240,7 @@ namespace VRCX
|
||||
/// Updates the child processes list.
|
||||
/// Removes any processes that have exited.
|
||||
/// </summary>
|
||||
internal void UpdateChildProcesses()
|
||||
private void UpdateChildProcesses()
|
||||
{
|
||||
foreach (var pair in startedProcesses.ToArray())
|
||||
{
|
||||
@@ -269,17 +269,17 @@ namespace VRCX
|
||||
/// <returns>
|
||||
/// <c>true</c> if child process running; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
internal bool IsChildProcessRunning(string path)
|
||||
private bool IsChildProcessRunning(string path)
|
||||
{
|
||||
return startedProcesses.ContainsKey(path);
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
public void Init()
|
||||
{
|
||||
// What are you lookin at? :eyes:
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
public void Exit()
|
||||
{
|
||||
childUpdateTimer.Stop();
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace VRCX
|
||||
}
|
||||
|
||||
// forgive me father for i have sinned once again
|
||||
AppApi.Instance.ExecuteAppFunction("dragEnterCef", file);
|
||||
Program.AppApiInstance.ExecuteAppFunction("dragEnterCef", file);
|
||||
dragData.Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using CefSharp;
|
||||
using CefSharp.SchemeHandler;
|
||||
using CefSharp.WinForms;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class CefService
|
||||
{
|
||||
public static readonly CefService Instance;
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetLogger("VRCX");
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static CefService()
|
||||
{
|
||||
|
||||
@@ -7,14 +7,14 @@ namespace VRCX
|
||||
public static void ApplyAppJavascriptBindings(IJavascriptObjectRepository repository)
|
||||
{
|
||||
repository.NameConverter = null;
|
||||
repository.Register("AppApi", AppApi.Instance);
|
||||
repository.Register("AppApi", Program.AppApiInstance);
|
||||
repository.Register("SharedVariable", SharedVariable.Instance);
|
||||
repository.Register("WebApi", WebApi.Instance);
|
||||
repository.Register("VRCXStorage", VRCXStorage.Instance);
|
||||
repository.Register("SQLite", SQLiteLegacy.Instance);
|
||||
repository.Register("LogWatcher", LogWatcher.Instance);
|
||||
repository.Register("Discord", Discord.Instance);
|
||||
repository.Register("AssetBundleCacher", AssetBundleCacher.Instance);
|
||||
repository.Register("AssetBundleManager", AssetBundleManager.Instance);
|
||||
}
|
||||
|
||||
public static void ApplyVrJavascriptBindings(IJavascriptObjectRepository repository)
|
||||
@@ -11,6 +11,7 @@ using System.Reflection;
|
||||
using System.Windows.Forms;
|
||||
using CefSharp;
|
||||
using CefSharp.WinForms;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
@@ -18,7 +19,7 @@ namespace VRCX
|
||||
{
|
||||
public static MainForm Instance;
|
||||
public static NativeWindow nativeWindow;
|
||||
private static NLog.Logger jslogger = NLog.LogManager.GetLogger("Javascript");
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
public ChromiumWebBrowser Browser;
|
||||
private readonly Timer _saveTimer;
|
||||
private int LastLocationX;
|
||||
@@ -59,7 +60,7 @@ namespace VRCX
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
jslogger.Error(ex);
|
||||
logger.Error(ex);
|
||||
}
|
||||
|
||||
Browser = new ChromiumWebBrowser("file://vrcx/index.html")
|
||||
@@ -83,7 +84,7 @@ namespace VRCX
|
||||
JavascriptBindings.ApplyAppJavascriptBindings(Browser.JavascriptObjectRepository);
|
||||
Browser.ConsoleMessage += (_, args) =>
|
||||
{
|
||||
jslogger.Debug(args.Message + " (" + args.Source + ":" + args.Line + ")");
|
||||
logger.Debug(args.Message + " (" + args.Source + ":" + args.Line + ")");
|
||||
};
|
||||
|
||||
Controls.Add(Browser);
|
||||
@@ -112,7 +113,7 @@ namespace VRCX
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
jslogger.Error(ex);
|
||||
logger.Error(ex);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -142,7 +143,7 @@ namespace VRCX
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
jslogger.Error(ex);
|
||||
logger.Error(ex);
|
||||
}
|
||||
|
||||
LastWindowStateToRestore = WindowState;
|
||||
@@ -32,12 +32,12 @@ namespace VRCX
|
||||
m_Timer = new Timer(TimerCallback, null, -1, -1);
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
public void Init()
|
||||
{
|
||||
m_Timer.Change(0, 1000);
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
public void Exit()
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
|
||||
@@ -9,10 +9,13 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
#if !LINUX
|
||||
using CefSharp;
|
||||
#endif
|
||||
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class IPCClient
|
||||
@@ -84,8 +87,11 @@ namespace VRCX
|
||||
{
|
||||
if (string.IsNullOrEmpty(packet))
|
||||
continue;
|
||||
|
||||
#if !LINUX
|
||||
if (MainForm.Instance?.Browser != null && !MainForm.Instance.Browser.IsLoading && MainForm.Instance.Browser.CanExecuteJavascriptInMainFrame)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.ipcEvent", packet);
|
||||
#endif
|
||||
}
|
||||
|
||||
_currentPacket = string.Empty;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
@@ -28,6 +28,7 @@ internal static class ImageCache
|
||||
httpClientHandler.Proxy = WebApi.Proxy;
|
||||
|
||||
httpClient = new HttpClient(httpClientHandler);
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", Program.Version);
|
||||
}
|
||||
|
||||
public static async Task<string> GetImage(string url, string fileId, string version)
|
||||
@@ -56,15 +57,11 @@ internal static class ImageCache
|
||||
foreach (Cookie cookie in cookies)
|
||||
cookieString += $"{cookie.Name}={cookie.Value};";
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url)
|
||||
{
|
||||
Headers =
|
||||
{
|
||||
{ "Cookie", cookieString },
|
||||
{ "User-Agent", Program.Version }
|
||||
}
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
if (!string.IsNullOrEmpty(cookieString))
|
||||
request.Headers.Add("Cookie", cookieString);
|
||||
|
||||
using (var response = await httpClient.SendAsync(request))
|
||||
{
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -104,15 +101,11 @@ internal static class ImageCache
|
||||
foreach (Cookie cookie in cookies)
|
||||
cookieString += $"{cookie.Name}={cookie.Value};";
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url)
|
||||
{
|
||||
Headers =
|
||||
{
|
||||
{ "Cookie", cookieString },
|
||||
{ "User-Agent", Program.Version }
|
||||
}
|
||||
};
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
if (!string.IsNullOrEmpty(cookieString))
|
||||
request.Headers.Add("Cookie", cookieString);
|
||||
|
||||
using var response = await httpClient.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return false;
|
||||
|
||||
@@ -10,7 +10,7 @@ using System.Text;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public static class JsonSerializer
|
||||
public static class JsonFileSerializer
|
||||
{
|
||||
public static void Serialize<T>(string path, T obj)
|
||||
{
|
||||
@@ -5,12 +5,18 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
|
||||
#if !LINUX
|
||||
using CefSharp;
|
||||
#endif
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
@@ -20,16 +26,17 @@ namespace VRCX
|
||||
public class LogWatcher
|
||||
{
|
||||
public static readonly LogWatcher Instance;
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetLogger("VRCX");
|
||||
private readonly Dictionary<string, LogContext> m_LogContextMap; // <FileName, LogContext>
|
||||
private readonly DirectoryInfo m_LogDirectoryInfo;
|
||||
private readonly List<string[]> m_LogList;
|
||||
private readonly ReaderWriterLockSlim m_LogListLock;
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private Dictionary<string, LogContext> m_LogContextMap; // <FileName, LogContext>
|
||||
private DirectoryInfo m_LogDirectoryInfo;
|
||||
private List<string[]> m_LogList;
|
||||
private ReaderWriterLockSlim m_LogListLock;
|
||||
private bool m_FirstRun = true;
|
||||
private bool m_ResetLog;
|
||||
private Thread m_Thread;
|
||||
private DateTime tillDate = DateTime.UtcNow;
|
||||
public bool VrcClosedGracefully;
|
||||
private readonly ConcurrentQueue<string> m_LogQueue = new ConcurrentQueue<string>(); // for electron
|
||||
|
||||
// NOTE
|
||||
// FileSystemWatcher() is unreliable
|
||||
@@ -39,9 +46,9 @@ namespace VRCX
|
||||
Instance = new LogWatcher();
|
||||
}
|
||||
|
||||
private LogWatcher()
|
||||
public void Init()
|
||||
{
|
||||
var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat";
|
||||
var logPath = Program.AppApiInstance.GetVRChatAppDataLocation();
|
||||
m_LogDirectoryInfo = new DirectoryInfo(logPath);
|
||||
m_LogContextMap = new Dictionary<string, LogContext>();
|
||||
m_LogListLock = new ReaderWriterLockSlim();
|
||||
@@ -50,14 +57,10 @@ namespace VRCX
|
||||
{
|
||||
IsBackground = true
|
||||
};
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
{
|
||||
m_Thread.Start();
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
public void Exit()
|
||||
{
|
||||
var thread = m_Thread;
|
||||
m_Thread = null;
|
||||
@@ -290,9 +293,13 @@ namespace VRCX
|
||||
{
|
||||
if (!m_FirstRun)
|
||||
{
|
||||
var logLine = System.Text.Json.JsonSerializer.Serialize(item);
|
||||
var logLine = JsonSerializer.Serialize(item);
|
||||
#if LINUX
|
||||
m_LogQueue.Enqueue(logLine);
|
||||
#else
|
||||
if (MainForm.Instance != null && MainForm.Instance.Browser != null)
|
||||
MainForm.Instance.Browser.ExecuteScriptAsync("$app.addGameLogEvent", logLine);
|
||||
#endif
|
||||
}
|
||||
|
||||
m_LogList.Add(item);
|
||||
@@ -303,6 +310,16 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetLogLines()
|
||||
{
|
||||
// for electron
|
||||
var logLines = new List<string>();
|
||||
while (m_LogQueue.TryDequeue(out var logLine))
|
||||
logLines.Add(logLine);
|
||||
|
||||
return logLines;
|
||||
}
|
||||
|
||||
private string ConvertLogTimeToISO8601(string line)
|
||||
{
|
||||
// 2020.10.31 23:36:22
|
||||
@@ -673,7 +690,9 @@ namespace VRCX
|
||||
|
||||
var data = line.Substring(offset + 13);
|
||||
|
||||
#if !LINUX
|
||||
WorldDBManager.Instance.ProcessLogWorldDataRequest(data);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace VRCX
|
||||
private PerformanceCounter _performanceCounterCpuUsage;
|
||||
private PerformanceCounter _performanceCounterUpTime;
|
||||
private Thread _thread;
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetLogger("VRCX");
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static SystemMonitor()
|
||||
{
|
||||
@@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using CefSharp;
|
||||
using NLog;
|
||||
using SharpDX;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
@@ -26,7 +27,7 @@ namespace VRCX
|
||||
public class VRCXVR : VRCXVRInterface
|
||||
{
|
||||
public static VRCXVRInterface Instance;
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
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 };
|
||||
@@ -401,7 +402,7 @@ namespace VRCX
|
||||
if (isHmdAfk != IsHmdAfk)
|
||||
{
|
||||
IsHmdAfk = isHmdAfk;
|
||||
AppApi.Instance.CheckGameRunning();
|
||||
Program.AppApiInstance.CheckGameRunning();
|
||||
}
|
||||
|
||||
var headsetErr = ETrackedPropertyError.TrackedProp_Success;
|
||||
|
||||
@@ -11,6 +11,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using CefSharp;
|
||||
using NLog;
|
||||
using SharpDX;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
@@ -22,7 +23,7 @@ namespace VRCX
|
||||
public class VRCXVRLegacy : VRCXVRInterface
|
||||
{
|
||||
public static VRCXVRInterface Instance;
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
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 };
|
||||
@@ -329,7 +330,7 @@ namespace VRCX
|
||||
if (isHmdAfk != IsHmdAfk)
|
||||
{
|
||||
IsHmdAfk = isHmdAfk;
|
||||
AppApi.Instance.CheckGameRunning();
|
||||
Program.AppApiInstance.CheckGameRunning();
|
||||
}
|
||||
|
||||
var headsetErr = ETrackedPropertyError.TrackedProp_Success;
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Threading.Tasks;
|
||||
using CefSharp;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
@@ -16,7 +17,7 @@ namespace VRCX
|
||||
public static readonly WorldDBManager Instance;
|
||||
private readonly HttpListener listener;
|
||||
private readonly WorldDatabase worldDB;
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private const string WorldDBServerUrl = "http://127.0.0.1:22500/";
|
||||
|
||||
private string lastError = null;
|
||||
|
||||
@@ -21,11 +21,13 @@ namespace VRCX
|
||||
{
|
||||
private readonly Dictionary<string, MonitoredProcess> monitoredProcesses;
|
||||
private readonly Timer monitorProcessTimer;
|
||||
private readonly static Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
static ProcessMonitor()
|
||||
{
|
||||
Instance = new ProcessMonitor();
|
||||
Instance.ProcessStarted += Program.AppApiInstance.OnProcessStateChanged;
|
||||
Instance.ProcessExited += Program.AppApiInstance.OnProcessStateChanged;
|
||||
}
|
||||
|
||||
public ProcessMonitor()
|
||||
@@ -164,7 +166,7 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
internal class MonitoredProcess
|
||||
public class MonitoredProcess
|
||||
{
|
||||
public MonitoredProcess(Process process)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,6 @@ using NLog;
|
||||
using NLog.Targets;
|
||||
using System;
|
||||
using System.Data.SQLite;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
@@ -22,13 +21,17 @@ namespace VRCX
|
||||
public static string ConfigLocation { get; private set; }
|
||||
public static string Version { get; private set; }
|
||||
public static bool LaunchDebug;
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetLogger("VRCX");
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
#if !LINUX
|
||||
public static VRCXVRInterface VRCXVRInstance { get; private set; }
|
||||
#endif
|
||||
public static AppApi AppApiInstance { get; private set; }
|
||||
|
||||
private static void SetProgramDirectories()
|
||||
{
|
||||
if (string.IsNullOrEmpty(AppDataDirectory))
|
||||
AppDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "VRCX");
|
||||
AppDataDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"VRCX");
|
||||
|
||||
BaseDirectory = AppDomain.CurrentDomain.BaseDirectory;
|
||||
ConfigLocation = Path.Combine(AppDataDirectory, "VRCX.sqlite3");
|
||||
@@ -41,37 +44,56 @@ namespace VRCX
|
||||
if (File.Exists(Path.Combine(BaseDirectory, "VRCX.json")))
|
||||
{
|
||||
File.Move(Path.Combine(BaseDirectory, "VRCX.json"), Path.Combine(AppDataDirectory, "VRCX.json"));
|
||||
File.Copy(Path.Combine(AppDataDirectory, "VRCX.json"), Path.Combine(AppDataDirectory, "VRCX-backup.json"));
|
||||
File.Copy(Path.Combine(AppDataDirectory, "VRCX.json"),
|
||||
Path.Combine(AppDataDirectory, "VRCX-backup.json"));
|
||||
}
|
||||
|
||||
if (File.Exists(Path.Combine(BaseDirectory, "VRCX.sqlite3")))
|
||||
{
|
||||
File.Move(Path.Combine(BaseDirectory, "VRCX.sqlite3"), Path.Combine(AppDataDirectory, "VRCX.sqlite3"));
|
||||
File.Copy(Path.Combine(AppDataDirectory, "VRCX.sqlite3"), Path.Combine(AppDataDirectory, "VRCX-backup.sqlite3"));
|
||||
File.Move(Path.Combine(BaseDirectory, "VRCX.sqlite3"),
|
||||
Path.Combine(AppDataDirectory, "VRCX.sqlite3"));
|
||||
File.Copy(Path.Combine(AppDataDirectory, "VRCX.sqlite3"),
|
||||
Path.Combine(AppDataDirectory, "VRCX-backup.sqlite3"));
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate cache to userdata for Cef 115 update
|
||||
var oldCachePath = Path.Combine(AppDataDirectory, "cache");
|
||||
if (Directory.Exists(oldCachePath))
|
||||
var newCachePath = Path.Combine(AppDataDirectory, "userdata", "cache");
|
||||
if (Directory.Exists(oldCachePath) && !Directory.Exists(newCachePath))
|
||||
{
|
||||
var newCachePath = Path.Combine(AppDataDirectory, "userdata", "cache");
|
||||
if (Directory.Exists(newCachePath))
|
||||
Directory.Delete(newCachePath, true);
|
||||
Directory.CreateDirectory(Path.Combine(AppDataDirectory, "userdata"));
|
||||
Directory.Move(oldCachePath, newCachePath);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GetVersion()
|
||||
{
|
||||
var buildName = "VRCX";
|
||||
|
||||
try
|
||||
{
|
||||
Version = $"{buildName} {File.ReadAllText(Path.Combine(BaseDirectory, "Version"))}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Version = $"{buildName} Build";
|
||||
}
|
||||
|
||||
Version = Version.Replace("\r", "").Replace("\n", "");
|
||||
}
|
||||
|
||||
private static void ConfigureLogger()
|
||||
{
|
||||
NLog.LogManager.Setup().LoadConfiguration(builder =>
|
||||
LogManager.Setup().LoadConfiguration(builder =>
|
||||
{
|
||||
|
||||
var fileTarget = new FileTarget("fileTarget")
|
||||
{
|
||||
FileName = Path.Combine(AppDataDirectory, "logs", "VRCX.log"),
|
||||
//Layout = "${longdate} [${level:uppercase=true}] ${logger} - ${message} ${exception:format=tostring}",
|
||||
// Layout with padding between the level/logger and message so that the message always starts at the same column
|
||||
Layout = "${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
|
||||
Layout =
|
||||
"${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
|
||||
ArchiveFileName = Path.Combine(AppDataDirectory, "logs", "VRCX.{#}.log"),
|
||||
ArchiveNumbering = ArchiveNumberingMode.DateAndSequence,
|
||||
ArchiveEvery = FileArchivePeriod.Day,
|
||||
@@ -84,57 +106,68 @@ namespace VRCX
|
||||
AutoFlush = true,
|
||||
Encoding = System.Text.Encoding.UTF8
|
||||
};
|
||||
|
||||
if (Program.LaunchDebug)
|
||||
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
|
||||
|
||||
var consoleTarget = new ConsoleTarget("consoleTarget")
|
||||
{
|
||||
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
// Archive maximum of 3 files 10MB each, kept for a maximum of 7 days
|
||||
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
|
||||
#else
|
||||
builder.ForLogger().FilterMinLevel(LogLevel.Debug).WriteTo(fileTarget);
|
||||
#endif
|
||||
}
|
||||
Layout = "${longdate} [${level:uppercase=true:padding=-5}] ${logger:padding=-20} - ${message} ${exception:format=tostring}",
|
||||
DetectConsoleAvailable = true
|
||||
};
|
||||
builder.ForLogger("VRCX").FilterMinLevel(LogLevel.Info).WriteTo(consoleTarget);
|
||||
});
|
||||
}
|
||||
|
||||
#if !LINUX
|
||||
[STAThread]
|
||||
private static void Main()
|
||||
{
|
||||
if (Wine.GetIfWine())
|
||||
{
|
||||
MessageBox.Show(
|
||||
"VRCX Cef has detected Wine.\nPlease switch to our native Electron build for Linux.",
|
||||
"Wine Detected", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
}
|
||||
try
|
||||
{
|
||||
Run();
|
||||
}
|
||||
|
||||
#region Handle CEF Explosion
|
||||
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
logger.Error(e, "Handled Exception, Missing file found in Handle Cef Explosion.");
|
||||
|
||||
var result = MessageBox.Show("VRCX has encountered an error with the CefSharp backend,\nthis is typically caused by missing files or dependencies.\nWould you like to try autofix by automatically installing vc_redist?.", "VRCX CefSharp not found.", MessageBoxButtons.YesNo, MessageBoxIcon.Error);
|
||||
var result = MessageBox.Show(
|
||||
"VRCX has encountered an error with the CefSharp backend,\nthis is typically caused by missing files or dependencies.\nWould you like to try autofix by automatically installing vc_redist?.",
|
||||
"VRCX CefSharp not found.", MessageBoxButtons.YesNo, MessageBoxIcon.Error);
|
||||
switch (result)
|
||||
{
|
||||
case DialogResult.Yes:
|
||||
logger.Fatal("Handled Exception, user selected auto install of vc_redist.");
|
||||
Update.DownloadInstallRedist();
|
||||
MessageBox.Show(
|
||||
"vc_redist has finished installing, if the issue persists upon next restart, please reinstall VRCX From GitHub,\nVRCX Will now restart.", "vc_redist installation complete", MessageBoxButtons.OK);
|
||||
"vc_redist has finished installing, if the issue persists upon next restart, please reinstall VRCX From GitHub,\nVRCX Will now restart.",
|
||||
"vc_redist installation complete", MessageBoxButtons.OK);
|
||||
Thread.Sleep(5000);
|
||||
AppApi.Instance.RestartApplication(false);
|
||||
AppApiInstance.RestartApplication(false);
|
||||
break;
|
||||
|
||||
case DialogResult.No:
|
||||
logger.Fatal("Handled Exception, user chose manual.");
|
||||
MessageBox.Show("VRCX will now close, try reinstalling VRCX using the setup from Github as a potential fix.", "VRCX CefSharp not found", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBox.Show(
|
||||
"VRCX will now close, try reinstalling VRCX using the setup from Github as a potential fix.",
|
||||
"VRCX CefSharp not found", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
Thread.Sleep(5000);
|
||||
Environment.Exit(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Handle Database Error
|
||||
|
||||
catch (SQLiteException e)
|
||||
{
|
||||
logger.Fatal(e, "Unhandled SQLite Exception, closing.");
|
||||
@@ -146,61 +179,53 @@ namespace VRCX
|
||||
e, "Database error", MessageBoxButtons.YesNo, MessageBoxIcon.Error);
|
||||
if (messageBoxResult == DialogResult.Yes)
|
||||
{
|
||||
AppApi.Instance.OpenLink("https://github.com/vrcx-team/VRCX/wiki#how-to-repair-vrcx-database");
|
||||
AppApiInstance.OpenLink("https://github.com/vrcx-team/VRCX/wiki#how-to-repair-vrcx-database");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
var cpuError = WinApi.GetCpuErrorMessage();
|
||||
if (cpuError != null)
|
||||
{
|
||||
var messageBoxResult = MessageBox.Show(cpuError.Value.Item1, "Potentially Faulty CPU Detected", MessageBoxButtons.YesNo, MessageBoxIcon.Error);
|
||||
var messageBoxResult = MessageBox.Show(cpuError.Value.Item1, "Potentially Faulty CPU Detected",
|
||||
MessageBoxButtons.YesNo, MessageBoxIcon.Error);
|
||||
if (messageBoxResult == DialogResult.Yes)
|
||||
{
|
||||
AppApi.Instance.OpenLink(cpuError.Value.Item2);
|
||||
AppApiInstance.OpenLink(cpuError.Value.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
logger.Fatal(e, "Unhandled Exception, program dying");
|
||||
MessageBox.Show(e.ToString(), "PLEASE REPORT IN https://vrcx.app/discord", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
MessageBox.Show(e.ToString(), "PLEASE REPORT IN https://vrcx.app/discord", MessageBoxButtons.OK,
|
||||
MessageBoxIcon.Error);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GetVersion()
|
||||
{
|
||||
var buildName = "VRCX";
|
||||
try
|
||||
{
|
||||
Version = $"{buildName} {File.ReadAllText(Path.Combine(BaseDirectory, "Version"))}";
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
Version = $"{buildName} Build";
|
||||
}
|
||||
Version = Version.Replace("\r", "").Replace("\n", "");
|
||||
}
|
||||
|
||||
private static void Run()
|
||||
{
|
||||
StartupArgs.ArgsCheck();
|
||||
SetProgramDirectories();
|
||||
VRCXStorage.Load();
|
||||
VRCXStorage.Instance.Load();
|
||||
BrowserSubprocess.Start();
|
||||
ConfigureLogger();
|
||||
Update.Check();
|
||||
GetVersion();
|
||||
Update.Check();
|
||||
|
||||
Application.EnableVisualStyles();
|
||||
Application.SetCompatibleTextRenderingDefault(false);
|
||||
|
||||
logger.Info("{0} Starting...", Version);
|
||||
logger.Debug("Wine support detection: {0}", Wine.GetIfWine());
|
||||
|
||||
ProcessMonitor.Instance.Init();
|
||||
logger.Debug("Wine detection: {0}", Wine.GetIfWine());
|
||||
|
||||
SQLiteLegacy.Instance.Init();
|
||||
AppApi.Instance.Init();
|
||||
AppApiInstance = new AppApiCef();
|
||||
|
||||
AppApiVr.Instance.Init();
|
||||
ProcessMonitor.Instance.Init();
|
||||
Discord.Instance.Init();
|
||||
WorldDBManager.Instance.Init();
|
||||
WebApi.Instance.Init();
|
||||
@@ -208,13 +233,13 @@ namespace VRCX
|
||||
AutoAppLaunchManager.Instance.Init();
|
||||
CefService.Instance.Init();
|
||||
IPCServer.Instance.Init();
|
||||
|
||||
|
||||
if (VRCXStorage.Instance.Get("VRCX_DisableVrOverlayGpuAcceleration") == "true")
|
||||
VRCXVRInstance = new VRCXVRLegacy();
|
||||
else
|
||||
VRCXVRInstance = new VRCXVR();
|
||||
VRCXVRInstance.Init();
|
||||
|
||||
|
||||
Application.Run(new MainForm());
|
||||
logger.Info("{0} Exiting...", Version);
|
||||
WebApi.Instance.SaveCookies();
|
||||
@@ -228,9 +253,43 @@ namespace VRCX
|
||||
|
||||
Discord.Instance.Exit();
|
||||
SystemMonitor.Instance.Exit();
|
||||
VRCXStorage.Save();
|
||||
VRCXStorage.Instance.Save();
|
||||
SQLiteLegacy.Instance.Exit();
|
||||
ProcessMonitor.Instance.Exit();
|
||||
}
|
||||
#else
|
||||
public static void PreInit(string version)
|
||||
{
|
||||
Version = version;
|
||||
StartupArgs.ArgsCheck();
|
||||
SetProgramDirectories();
|
||||
}
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
ConfigureLogger();
|
||||
Update.Check();
|
||||
|
||||
logger.Info("{0} Starting...", Version);
|
||||
|
||||
AppApiInstance = new AppApiElectron();
|
||||
// ProcessMonitor.Instance.Init();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if LINUX
|
||||
public class ProgramElectron
|
||||
{
|
||||
public void PreInit(string version)
|
||||
{
|
||||
Program.PreInit(version);
|
||||
}
|
||||
|
||||
public void Init()
|
||||
{
|
||||
Program.Init();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
using CefSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data.SQLite;
|
||||
using System.IO;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class SQLiteLegacy
|
||||
{
|
||||
public static readonly SQLiteLegacy Instance;
|
||||
public static SQLiteLegacy Instance;
|
||||
private readonly ReaderWriterLockSlim m_ConnectionLock;
|
||||
private SQLiteConnection m_Connection;
|
||||
|
||||
@@ -23,8 +24,11 @@ namespace VRCX
|
||||
m_ConnectionLock = new ReaderWriterLockSlim();
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
public void Init()
|
||||
{
|
||||
#if LINUX
|
||||
Instance = this;
|
||||
#endif
|
||||
var dataSource = Program.ConfigLocation;
|
||||
var jsonDataSource = VRCXStorage.Instance.Get("VRCX_DatabaseLocation");
|
||||
if (!string.IsNullOrEmpty(jsonDataSource))
|
||||
@@ -35,89 +39,60 @@ namespace VRCX
|
||||
m_Connection.Open();
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
public void Exit()
|
||||
{
|
||||
m_Connection.Close();
|
||||
m_Connection.Dispose();
|
||||
}
|
||||
|
||||
public void Execute(IJavascriptCallback callback, string sql, IDictionary<string, object> args = null)
|
||||
|
||||
public string ExecuteJson(string sql, IDictionary<string, object> args = null)
|
||||
{
|
||||
try
|
||||
var result = Execute(sql, args);
|
||||
if (result.Item1 != null)
|
||||
{
|
||||
m_ConnectionLock.EnterReadLock();
|
||||
try
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
using (var command = new SQLiteCommand(sql, m_Connection))
|
||||
{
|
||||
if (args != null)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
}
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read() == true)
|
||||
{
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(null, values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(null, null);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
m_ConnectionLock.ExitReadLock();
|
||||
}
|
||||
status = "error",
|
||||
message = result.Item1
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
return JsonSerializer.Serialize(new
|
||||
{
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(e.Message, null);
|
||||
}
|
||||
}
|
||||
|
||||
callback.Dispose();
|
||||
status = "success",
|
||||
data = result.Item2
|
||||
});
|
||||
}
|
||||
|
||||
public void Execute(Action<object[]> callback, string sql, IDictionary<string, object> args = null)
|
||||
public Tuple<string, object[]> Execute(string sql, IDictionary<string, object> args = null)
|
||||
{
|
||||
m_ConnectionLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
using (var command = new SQLiteCommand(sql, m_Connection))
|
||||
using var command = new SQLiteCommand(sql, m_Connection);
|
||||
if (args != null)
|
||||
{
|
||||
if (args != null)
|
||||
foreach (var arg in args)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
}
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read() == true)
|
||||
{
|
||||
var values = new object[reader.FieldCount];
|
||||
reader.GetValues(values);
|
||||
callback(values);
|
||||
}
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
}
|
||||
|
||||
using var reader = command.ExecuteReader();
|
||||
var result = new List<object[]>();
|
||||
while (reader.Read())
|
||||
{
|
||||
var values = new object[reader.FieldCount];
|
||||
for (var i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
values[i] = reader.GetValue(i);
|
||||
}
|
||||
result.Add(values);
|
||||
}
|
||||
return new Tuple<string, object[]>(null, result.ToArray());
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new Tuple<string, object[]>(ex.Message, null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -128,21 +103,18 @@ namespace VRCX
|
||||
public int ExecuteNonQuery(string sql, IDictionary<string, object> args = null)
|
||||
{
|
||||
int result = -1;
|
||||
|
||||
m_ConnectionLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
using (var command = new SQLiteCommand(sql, m_Connection))
|
||||
using var command = new SQLiteCommand(sql, m_Connection);
|
||||
if (args != null)
|
||||
{
|
||||
if (args != null)
|
||||
foreach (var arg in args)
|
||||
{
|
||||
foreach (var arg in args)
|
||||
{
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
command.Parameters.Add(new SQLiteParameter(arg.Key, arg.Value));
|
||||
}
|
||||
result = command.ExecuteNonQuery();
|
||||
}
|
||||
result = command.ExecuteNonQuery();
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -13,9 +13,8 @@ namespace VRCX
|
||||
{
|
||||
internal static class ScreenshotHelper
|
||||
{
|
||||
private static readonly ILogger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly byte[] pngSignatureBytes = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
private static readonly ScreenshotMetadataDatabase cacheDatabase = new ScreenshotMetadataDatabase(System.IO.Path.Combine(Program.AppDataDirectory, "metadataCache.db"));
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly ScreenshotMetadataDatabase cacheDatabase = new ScreenshotMetadataDatabase(Path.Combine(Program.AppDataDirectory, "metadataCache.db"));
|
||||
private static readonly Dictionary<string, ScreenshotMetadata> metadataCache = new Dictionary<string, ScreenshotMetadata>();
|
||||
|
||||
public enum ScreenshotSearchType
|
||||
@@ -385,6 +384,8 @@ namespace VRCX
|
||||
/// <returns></returns>
|
||||
public static bool IsPNGFile(string path)
|
||||
{
|
||||
var pngSignatureBytes = new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
|
||||
|
||||
// Read only the first 8 bytes of the file to check if it's a PNG file instead of reading the entire thing into memory just to see check a couple bytes.
|
||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
{
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
// This work is licensed under the terms of the MIT license.
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using CefSharp.Internals;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@@ -13,12 +12,17 @@ using System.Linq;
|
||||
using System.Management;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
#if !LINUX
|
||||
using System.Windows.Forms;
|
||||
using CefSharp.Internals;
|
||||
#endif
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class StartupArgs
|
||||
{
|
||||
private const string SubProcessTypeArgument = "--type";
|
||||
public static VrcxLaunchArguments LaunchArguments = new();
|
||||
|
||||
public static void ArgsCheck()
|
||||
@@ -37,13 +41,19 @@ namespace VRCX
|
||||
{
|
||||
if (File.Exists(LaunchArguments.ConfigDirectory))
|
||||
{
|
||||
MessageBox.Show("Move your \"VRCX.sqlite3\" into a folder then specify the folder in the launch parameter e.g.\n--config=\"C:\\VRCX\\\"", "--config is now a directory", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
var message =
|
||||
"Move your \"VRCX.sqlite3\" into a folder then specify the folder in the launch parameter e.g.\n--config=\"C:\\VRCX\\\"";
|
||||
#if !LINUX
|
||||
MessageBox.Show(message, "--config is now a directory", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
#endif
|
||||
Console.WriteLine(message);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
Program.AppDataDirectory = LaunchArguments.ConfigDirectory;
|
||||
}
|
||||
|
||||
#if !LINUX
|
||||
var disableClosing = LaunchArguments.IsUpgrade || // we're upgrading, allow it
|
||||
!string.IsNullOrEmpty(CommandLineArgsParser.GetArgumentValue(args, CefSharpArguments.SubProcessTypeArgument)); // we're launching a subprocess, allow it
|
||||
|
||||
@@ -54,6 +64,7 @@ namespace VRCX
|
||||
Thread.Sleep(10);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private static VrcxLaunchArguments ParseArgs(string[] args)
|
||||
@@ -119,7 +130,7 @@ namespace VRCX
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (commandLine.Contains(CefSharpArguments.SubProcessTypeArgument)) // ignore subprocesses
|
||||
if (commandLine.Contains(SubProcessTypeArgument)) // ignore subprocesses
|
||||
continue;
|
||||
|
||||
var processArguments = ParseArgs(commandLine.Split(' '));
|
||||
|
||||
277
Dotnet/Update.cs
@@ -5,48 +5,88 @@
|
||||
// For a copy, see <https://opensource.org/licenses/MIT>.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows.Forms;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NLog;
|
||||
|
||||
#if !LINUX
|
||||
using System.Windows.Forms;
|
||||
#endif
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
internal class Update
|
||||
public class Update
|
||||
{
|
||||
private static readonly string VRCX_Setup_Executable = Path.Combine(Program.AppDataDirectory, "VRCX_Setup.exe");
|
||||
private static readonly string Update_Executable = Path.Combine(Program.AppDataDirectory, "update.exe");
|
||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger();
|
||||
private static readonly string VrcxSetupExecutable = Path.Combine(Program.AppDataDirectory, "VRCX_Setup.exe");
|
||||
private static readonly string UpdateExecutable = Path.Combine(Program.AppDataDirectory, "update.exe");
|
||||
private static readonly string TempDownload = Path.Combine(Program.AppDataDirectory, "tempDownload");
|
||||
private static readonly string HashLocation = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
|
||||
private static readonly HttpClient httpClient;
|
||||
private static CancellationToken _cancellationToken;
|
||||
public static int UpdateProgress;
|
||||
private static string AppImagePath = string.Empty;
|
||||
private static string AppImagePathOld = string.Empty;
|
||||
|
||||
static Update()
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
if (WebApi.ProxySet)
|
||||
httpClientHandler.Proxy = WebApi.Proxy;
|
||||
|
||||
httpClient = new HttpClient(httpClientHandler);
|
||||
httpClient.DefaultRequestHeaders.Add("User-Agent", Program.Version);
|
||||
}
|
||||
|
||||
public void Init(string appImagePath = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(appImagePath))
|
||||
return;
|
||||
|
||||
AppImagePath = appImagePath;
|
||||
AppImagePathOld = appImagePath + ".old";
|
||||
logger.Info($"AppImagePath: {AppImagePath}");
|
||||
}
|
||||
|
||||
public static void Check()
|
||||
{
|
||||
if (Process.GetProcessesByName("VRCX_Setup").Length > 0)
|
||||
Environment.Exit(0);
|
||||
var setupHash = Path.Combine(Program.AppDataDirectory, "sha256sum.txt");
|
||||
if (File.Exists(setupHash))
|
||||
File.Delete(setupHash);
|
||||
var tempDownload = Path.Combine(Program.AppDataDirectory, "tempDownload.exe");
|
||||
if (File.Exists(tempDownload))
|
||||
File.Delete(tempDownload);
|
||||
if (File.Exists(VRCX_Setup_Executable))
|
||||
File.Delete(VRCX_Setup_Executable);
|
||||
if (File.Exists(Update_Executable))
|
||||
Install();
|
||||
|
||||
if (File.Exists(HashLocation))
|
||||
File.Delete(HashLocation);
|
||||
if (File.Exists(TempDownload))
|
||||
File.Delete(TempDownload);
|
||||
if (File.Exists(VrcxSetupExecutable))
|
||||
File.Delete(VrcxSetupExecutable);
|
||||
|
||||
if (File.Exists(UpdateExecutable))
|
||||
InstallUpdate();
|
||||
}
|
||||
|
||||
private static void Install()
|
||||
private static void InstallUpdate()
|
||||
{
|
||||
var setupArguments = string.Empty;
|
||||
#if !LINUX
|
||||
if (Wine.GetIfWine())
|
||||
setupArguments += "/SKIP_SHORTCUT=true";
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
File.Move(Update_Executable, VRCX_Setup_Executable);
|
||||
if (File.Exists(VrcxSetupExecutable))
|
||||
File.Delete(VrcxSetupExecutable);
|
||||
File.Move(UpdateExecutable, VrcxSetupExecutable);
|
||||
var vrcxProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = VRCX_Setup_Executable,
|
||||
FileName = VrcxSetupExecutable,
|
||||
Arguments = setupArguments,
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = Program.AppDataDirectory
|
||||
@@ -57,15 +97,19 @@ namespace VRCX
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MessageBox.Show(e.ToString(), "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
var message = $"Failed to install the update: {e.Message}";
|
||||
logger.Info(message);
|
||||
#if !LINUX
|
||||
MessageBox.Show(message, "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public static void DownloadInstallRedist()
|
||||
public static async Task DownloadInstallRedist()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filePath = DownloadFile("https://aka.ms/vs/17/release/vc_redist.x64.exe");
|
||||
var filePath = await DownloadFile("https://aka.ms/vs/17/release/vc_redist.x64.exe");
|
||||
var installRedist = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
@@ -75,55 +119,31 @@ namespace VRCX
|
||||
}
|
||||
};
|
||||
installRedist.Start();
|
||||
installRedist.WaitForExit();
|
||||
await installRedist.WaitForExitAsync();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MessageBox.Show(e.ToString(), "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
var message = $"Failed to download and install the Visual C++ Redistributable: {e.Message}";
|
||||
logger.Info(message);
|
||||
#if !LINUX
|
||||
MessageBox.Show(message, "Update failed", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private static string DownloadFile(string fileUrl)
|
||||
private static async Task<string> DownloadFile(string fileUrl, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpClientHandler = new HttpClientHandler();
|
||||
if (WebApi.ProxySet)
|
||||
httpClientHandler.Proxy = WebApi.Proxy;
|
||||
|
||||
var httpClient = new HttpClient(httpClientHandler);
|
||||
var response = await httpClient.GetAsync(fileUrl, cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new Exception($"Failed to download the file. Status code: {response.StatusCode}");
|
||||
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = httpClient.GetAsync(fileUrl).Result;
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string fileName = GetFileNameFromContentDisposition(response);
|
||||
|
||||
string tempPath = Path.Combine(Path.GetTempPath(), "VRCX");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
|
||||
string filePath = Path.Combine(tempPath, fileName);
|
||||
|
||||
using (FileStream fileStream = File.Create(filePath))
|
||||
{
|
||||
response.Content.CopyToAsync(fileStream).Wait();
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"Failed to download the file. Status code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Error downloading the file: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.Dispose();
|
||||
}
|
||||
var fileName = GetFileNameFromContentDisposition(response);
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), "VRCX");
|
||||
Directory.CreateDirectory(tempPath);
|
||||
var filePath = Path.Combine(tempPath, fileName);
|
||||
await using var fileStream = File.Create(filePath);
|
||||
await response.Content.CopyToAsync(fileStream, cancellationToken);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static string GetFileNameFromContentDisposition(HttpResponseMessage response)
|
||||
@@ -135,7 +155,7 @@ namespace VRCX
|
||||
if (startIndex >= 0)
|
||||
{
|
||||
startIndex += "filename=".Length;
|
||||
int endIndex = contentDisposition.IndexOf(';', startIndex);
|
||||
int endIndex = contentDisposition.IndexOf(";", startIndex, StringComparison.Ordinal);
|
||||
if (endIndex == -1)
|
||||
{
|
||||
endIndex = contentDisposition.Length;
|
||||
@@ -148,5 +168,138 @@ namespace VRCX
|
||||
|
||||
throw new Exception("Unable to extract file name from content-disposition header.");
|
||||
}
|
||||
|
||||
public static async Task DownloadUpdate(string fileUrl, string fileName, string hashUrl, int downloadSize)
|
||||
{
|
||||
_cancellationToken = CancellationToken.None;
|
||||
const int chunkSize = 8192;
|
||||
|
||||
if (File.Exists(TempDownload))
|
||||
File.Delete(TempDownload);
|
||||
if (File.Exists(HashLocation))
|
||||
File.Delete(HashLocation);
|
||||
|
||||
var hashesPath = await DownloadFile(hashUrl, _cancellationToken);
|
||||
if (!string.IsNullOrEmpty(hashesPath))
|
||||
File.Move(hashesPath, HashLocation);
|
||||
|
||||
await using var destination = File.OpenWrite(TempDownload);
|
||||
using (var response = await httpClient.GetAsync(fileUrl, HttpCompletionOption.ResponseHeadersRead, _cancellationToken))
|
||||
await using (var download = await response.Content.ReadAsStreamAsync(_cancellationToken))
|
||||
{
|
||||
var contentLength = response.Content.Headers.ContentLength;
|
||||
var buffer = new byte[chunkSize];
|
||||
long totalBytesRead = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int bytesRead = await download.ReadAsync(buffer, 0, chunkSize, _cancellationToken);
|
||||
if (bytesRead == 0) break;
|
||||
|
||||
if (_cancellationToken.IsCancellationRequested)
|
||||
throw new OperationCanceledException("Download was cancelled.");
|
||||
|
||||
await destination.WriteAsync(buffer.AsMemory(0, bytesRead), _cancellationToken);
|
||||
totalBytesRead += bytesRead;
|
||||
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
double percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
|
||||
UpdateProgress = (int)percentage;
|
||||
}
|
||||
}
|
||||
|
||||
if (contentLength.HasValue)
|
||||
{
|
||||
double percentage = Math.Round((double)totalBytesRead / contentLength.Value * 100, 2);
|
||||
UpdateProgress = (int)percentage;
|
||||
}
|
||||
}
|
||||
destination.Close();
|
||||
|
||||
var data = new FileInfo(TempDownload);
|
||||
if (data.Length != downloadSize)
|
||||
{
|
||||
File.Delete(TempDownload);
|
||||
logger.Error("Downloaded file size does not match expected size");
|
||||
throw new Exception("Downloaded file size does not match expected size");
|
||||
}
|
||||
if (File.Exists(HashLocation))
|
||||
{
|
||||
logger.Info("Checking hash");
|
||||
var lines = await File.ReadAllLinesAsync(HashLocation, _cancellationToken);
|
||||
var hashDict = new Dictionary<string, string>();
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var split = line.Split(' ');
|
||||
if (split.Length == 3)
|
||||
hashDict[split[2]] = split[0];
|
||||
}
|
||||
using (var sha256 = SHA256.Create())
|
||||
await using (var stream = File.OpenRead(TempDownload))
|
||||
{
|
||||
var hashBytes = await sha256.ComputeHashAsync(stream, _cancellationToken);
|
||||
var hashString = BitConverter.ToString(hashBytes).Replace("-", "");
|
||||
if (!hashDict.TryGetValue(fileName, out var expectedHash))
|
||||
{
|
||||
logger.Error("Hash check failed, file not found in hash file");
|
||||
}
|
||||
if (!string.IsNullOrEmpty(expectedHash) &&
|
||||
!hashString.Equals(expectedHash, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
logger.Error($"Hash check failed file:{hashString} web:{expectedHash}");
|
||||
throw new Exception("Hash check failed");
|
||||
// can't delete file yet because it's in use
|
||||
}
|
||||
}
|
||||
File.Delete(HashLocation);
|
||||
logger.Info("Hash check passed");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(AppImagePath))
|
||||
{
|
||||
// Windows
|
||||
if (File.Exists(UpdateExecutable))
|
||||
File.Delete(UpdateExecutable);
|
||||
File.Move(TempDownload, UpdateExecutable);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Linux
|
||||
if (File.Exists(AppImagePathOld))
|
||||
File.Delete(AppImagePathOld);
|
||||
File.Move(AppImagePath, AppImagePathOld);
|
||||
File.Move(TempDownload, AppImagePath);
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "chmod",
|
||||
Arguments = $"+x {AppImagePath}"
|
||||
}
|
||||
};
|
||||
process.Start();
|
||||
await process.WaitForExitAsync();
|
||||
}
|
||||
|
||||
UpdateProgress = 0;
|
||||
_cancellationToken = CancellationToken.None;
|
||||
}
|
||||
|
||||
public static async Task CancelUpdate()
|
||||
{
|
||||
_cancellationToken = new CancellationToken(true);
|
||||
UpdateProgress = 0;
|
||||
await Task.Delay(100);
|
||||
try
|
||||
{
|
||||
if (File.Exists(TempDownload))
|
||||
File.Delete(TempDownload);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputPath>..\build\Cef\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFrameworks>net8-windows10.0.19041.0</TargetFrameworks>
|
||||
<TargetFramework>net8-windows10.0.19041.0</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
@@ -9,16 +12,21 @@
|
||||
<AssemblyTitle>VRCX</AssemblyTitle>
|
||||
<Product>VRCX</Product>
|
||||
<Copyright>vrcx-team, pypy, natsumi</Copyright>
|
||||
<OutputPath>bin\$(Platform)\$(Configuration)\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Title>VRCX</Title>
|
||||
<Description>VRCX</Description>
|
||||
<PackageIcon>VRCX.png</PackageIcon>
|
||||
<PackageIcon>..\VRCX.png</PackageIcon>
|
||||
<RepositoryUrl>https://github.com/vrcx-team/VRCX</RepositoryUrl>
|
||||
<ResourceLanguages>en</ResourceLanguages>
|
||||
<SatelliteResourceLanguages>en-US;en</SatelliteResourceLanguages>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
<AssemblyName>VRCX</AssemblyName>
|
||||
<RootNamespace>VRCX</RootNamespace>
|
||||
<PackageId>VRCX</PackageId>
|
||||
<Company>VRCX</Company>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
@@ -29,7 +37,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>VRCX.ico</ApplicationIcon>
|
||||
<ApplicationIcon>..\VRCX.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -59,7 +67,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.manifest" />
|
||||
<None Include="Version">
|
||||
<None Include="..\Version">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
@@ -68,10 +76,10 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>openvr_api.dll</TargetPath>
|
||||
</None>
|
||||
<Content Include="VRCX.ico">
|
||||
<Content Include="..\VRCX.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="VRCX.png">
|
||||
<Content Include="..\VRCX.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
@@ -100,6 +108,11 @@
|
||||
</Target>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
<Exec Command="$(ProjectDir)/build-tools/Topten.nvpatch.exe --enable VRCX.exe" WorkingDirectory="$(OutDir)" />
|
||||
<Exec Command="$(ProjectDir)..\build-tools\Topten.nvpatch.exe --enable VRCX.exe" WorkingDirectory="$(OutDir)" />
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="obj1\**" />
|
||||
<Compile Remove="obj1\**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
126
Dotnet/VRCX-Electron.csproj
Normal file
@@ -0,0 +1,126 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<BaseIntermediateOutputPath>obj1\</BaseIntermediateOutputPath>
|
||||
<OutputPath>..\build\Electron\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Platforms>x64</Platforms>
|
||||
<ApplicationRevision>0</ApplicationRevision>
|
||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||
<AssemblyTitle>VRCX</AssemblyTitle>
|
||||
<Product>VRCX</Product>
|
||||
<Copyright>vrcx-team, pypy, natsumi</Copyright>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Title>VRCX</Title>
|
||||
<Description>VRCX</Description>
|
||||
<PackageIcon>..\VRCX.png</PackageIcon>
|
||||
<RepositoryUrl>https://github.com/vrcx-team/VRCX</RepositoryUrl>
|
||||
<ResourceLanguages>en</ResourceLanguages>
|
||||
<SatelliteResourceLanguages>en-US;en</SatelliteResourceLanguages>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||
</PropertyGroup>
|
||||
<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" />
|
||||
|
||||
<PropertyGroup>
|
||||
<DefineConstants>LINUX</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x64'">
|
||||
<DebugType>full</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Release|x64'">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<AssemblyName>VRCX</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>..\VRCX.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(PlatformTarget)' == 'x64'">
|
||||
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier>
|
||||
<SelfContained Condition="'$(SelfContained)' == ''">false</SelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ContentSQLiteInteropFiles>true</ContentSQLiteInteropFiles>
|
||||
<CopySQLiteInteropFiles>true</CopySQLiteInteropFiles>
|
||||
<CleanSQLiteInteropFiles>false</CleanSQLiteInteropFiles>
|
||||
<CollectSQLiteInteropFiles>false</CollectSQLiteInteropFiles>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Blake2Sharp">
|
||||
<HintPath>libs\Blake2Sharp.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="librsync.net">
|
||||
<HintPath>libs\librsync.net.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="app.manifest" />
|
||||
<None Include="..\Version">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="libs\openvr_api.dll">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>openvr_api.dll</TargetPath>
|
||||
</None>
|
||||
<None Include="libs\linux\SQLite.Interop.dll" Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>SQLite.Interop.dll</TargetPath>
|
||||
</None>
|
||||
<None Include="libs\linux\System.Data.SQLite.dll" Condition="$([MSBuild]::IsOSPlatform('Linux'))">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>System.Data.SQLite.dll</TargetPath>
|
||||
</None>
|
||||
<Content Include="..\VRCX.ico">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\VRCX.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.JavaScript.NodeApi" Version="0.8.16" />
|
||||
<PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.8.16" />
|
||||
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="NLog" Version="5.3.4" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Mathematics" Version="4.2.0" />
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
|
||||
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
|
||||
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.119" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="9.0.0" />
|
||||
<PackageReference Include="System.Management" Version="9.0.0" />
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="Websocket.Client" Version="5.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Remove="obj\**" />
|
||||
<Compile Remove="obj\**" />
|
||||
<Content Remove="Cef\**" />
|
||||
<Compile Remove="Cef\**" />
|
||||
<Content Remove="AppApi\Cef\**" />
|
||||
<Compile Remove="AppApi\Cef\**" />
|
||||
<Content Remove="PWI\**" />
|
||||
<Compile Remove="PWI\**" />
|
||||
<Content Remove="Overlay\**" />
|
||||
<Compile Remove="Overlay\**" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -7,6 +7,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
namespace VRCX
|
||||
@@ -24,12 +25,12 @@ namespace VRCX
|
||||
Instance = new VRCXStorage();
|
||||
}
|
||||
|
||||
public static void Load()
|
||||
public void Load()
|
||||
{
|
||||
m_Lock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
JsonSerializer.Deserialize(m_JsonPath, ref m_Storage);
|
||||
JsonFileSerializer.Deserialize(m_JsonPath, ref m_Storage);
|
||||
m_Dirty = false;
|
||||
}
|
||||
finally
|
||||
@@ -38,14 +39,14 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
public void Save()
|
||||
{
|
||||
m_Lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (m_Dirty)
|
||||
{
|
||||
JsonSerializer.Serialize(m_JsonPath, m_Storage);
|
||||
JsonFileSerializer.Serialize(m_JsonPath, m_Storage);
|
||||
m_Dirty = false;
|
||||
}
|
||||
}
|
||||
@@ -129,7 +130,7 @@ namespace VRCX
|
||||
m_Lock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
return System.Text.Json.JsonSerializer.Serialize(m_Storage);
|
||||
return JsonSerializer.Serialize(m_Storage);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
274
Dotnet/WebApi.cs
@@ -1,22 +1,26 @@
|
||||
using CefSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Cookie = System.Net.Cookie;
|
||||
using System.Windows;
|
||||
using NLog;
|
||||
using Timer = System.Threading.Timer;
|
||||
|
||||
#if !LINUX
|
||||
using CefSharp;
|
||||
using System.Windows.Forms;
|
||||
#endif
|
||||
|
||||
namespace VRCX
|
||||
{
|
||||
public class WebApi
|
||||
{
|
||||
public static readonly WebApi Instance;
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
public static WebApi Instance;
|
||||
|
||||
public static bool ProxySet;
|
||||
public static string ProxyUrl = "";
|
||||
@@ -35,6 +39,10 @@ namespace VRCX
|
||||
|
||||
public WebApi()
|
||||
{
|
||||
#if LINUX
|
||||
if (Instance == null)
|
||||
Instance = this;
|
||||
#endif
|
||||
_cookieContainer = new CookieContainer();
|
||||
_timer = new Timer(TimerCallback, null, -1, -1);
|
||||
}
|
||||
@@ -45,12 +53,13 @@ namespace VRCX
|
||||
{
|
||||
SaveCookies();
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"Failed to save cookies: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void Init()
|
||||
public void Init()
|
||||
{
|
||||
SetProxy();
|
||||
LoadCookies();
|
||||
@@ -80,12 +89,16 @@ namespace VRCX
|
||||
catch (UriFormatException)
|
||||
{
|
||||
VRCXStorage.Instance.Set("VRCX_ProxyServer", string.Empty);
|
||||
MessageBox.Show("The proxy server URI you used is invalid.\nVRCX will close, please correct the proxy URI.", "Invalid Proxy URI", MessageBoxButton.OK);
|
||||
var 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);
|
||||
#endif
|
||||
Logger.Error(message);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
internal void Exit()
|
||||
public void Exit()
|
||||
{
|
||||
_timer.Change(-1, -1);
|
||||
SaveCookies();
|
||||
@@ -97,32 +110,30 @@ namespace VRCX
|
||||
SaveCookies();
|
||||
}
|
||||
|
||||
internal void LoadCookies()
|
||||
private void LoadCookies()
|
||||
{
|
||||
SQLiteLegacy.Instance.ExecuteNonQuery("CREATE TABLE IF NOT EXISTS `cookies` (`key` TEXT PRIMARY KEY, `value` TEXT)");
|
||||
SQLiteLegacy.Instance.Execute((values) =>
|
||||
{
|
||||
try
|
||||
var values = SQLiteLegacy.Instance.Execute("SELECT `value` FROM `cookies` WHERE `key` = @key",
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String((string)values[0])))
|
||||
{
|
||||
_cookieContainer = new CookieContainer();
|
||||
_cookieContainer.Add(System.Text.Json.JsonSerializer.Deserialize<CookieCollection>(stream));
|
||||
//_cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(stream);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
},
|
||||
"SELECT `value` FROM `cookies` WHERE `key` = @key",
|
||||
new Dictionary<string, object>() {
|
||||
{"@key", "default"}
|
||||
{ "@key", "default" }
|
||||
}
|
||||
);
|
||||
try
|
||||
{
|
||||
var item = (object[])values.Item2[0];
|
||||
using var stream = new MemoryStream(Convert.FromBase64String((string)item[0]));
|
||||
_cookieContainer = new CookieContainer();
|
||||
_cookieContainer.Add(System.Text.Json.JsonSerializer.Deserialize<CookieCollection>(stream));
|
||||
// _cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(stream); // from .NET framework
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"Failed to load cookies: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
internal void SaveCookies()
|
||||
public void SaveCookies()
|
||||
{
|
||||
if (_cookieDirty == false)
|
||||
{
|
||||
@@ -148,8 +159,9 @@ namespace VRCX
|
||||
}
|
||||
_cookieDirty = false;
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error($"Failed to save cookies: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,19 +169,15 @@ namespace VRCX
|
||||
{
|
||||
_cookieDirty = true; // force cookies to be saved for lastUserLoggedIn
|
||||
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
System.Text.Json.JsonSerializer.Serialize(memoryStream, _cookieContainer.GetAllCookies());
|
||||
//new BinaryFormatter().Serialize(memoryStream, _cookieContainer);
|
||||
return Convert.ToBase64String(memoryStream.ToArray());
|
||||
}
|
||||
using var memoryStream = new MemoryStream();
|
||||
System.Text.Json.JsonSerializer.Serialize(memoryStream, _cookieContainer.GetAllCookies());
|
||||
return Convert.ToBase64String(memoryStream.ToArray());
|
||||
}
|
||||
|
||||
public void SetCookies(string cookies)
|
||||
{
|
||||
using (var stream = new MemoryStream(Convert.FromBase64String(cookies)))
|
||||
{
|
||||
//_cookieContainer = (CookieContainer)new BinaryFormatter().Deserialize(stream);
|
||||
_cookieContainer = new CookieContainer();
|
||||
_cookieContainer.Add(System.Text.Json.JsonSerializer.Deserialize<CookieCollection>(stream));
|
||||
}
|
||||
@@ -200,7 +208,7 @@ namespace VRCX
|
||||
}
|
||||
}
|
||||
var imageData = options["imageData"] as string;
|
||||
byte[] fileToUpload = AppApi.Instance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), false);
|
||||
byte[] fileToUpload = Program.AppApiInstance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), false);
|
||||
string fileFormKey = "image";
|
||||
string fileName = "image.png";
|
||||
string fileMimeType = "image/png";
|
||||
@@ -269,7 +277,7 @@ namespace VRCX
|
||||
}
|
||||
var imageData = options["imageData"] as string;
|
||||
var matchingDimensions = options["matchingDimensions"] as bool? ?? false;
|
||||
byte[] fileToUpload = AppApi.Instance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), matchingDimensions);
|
||||
byte[] fileToUpload = Program.AppApiInstance.ResizeImageToFitLimits(Convert.FromBase64String(imageData), matchingDimensions);
|
||||
|
||||
string fileFormKey = "file";
|
||||
string fileName = "blob";
|
||||
@@ -306,7 +314,7 @@ namespace VRCX
|
||||
request.ContentType = "multipart/form-data; boundary=" + boundary;
|
||||
var requestStream = request.GetRequestStream();
|
||||
var imageData = options["imageData"] as string;
|
||||
var fileToUpload = AppApi.Instance.ResizePrintImage(Convert.FromBase64String(imageData));
|
||||
var fileToUpload = Program.AppApiInstance.ResizePrintImage(Convert.FromBase64String(imageData));
|
||||
const string fileFormKey = "image";
|
||||
const string fileName = "image";
|
||||
const string fileMimeType = "image/png";
|
||||
@@ -347,159 +355,135 @@ namespace VRCX
|
||||
await requestStream.WriteAsync(endBytes);
|
||||
requestStream.Close();
|
||||
}
|
||||
|
||||
public async Task<string> ExecuteJson(string options)
|
||||
{
|
||||
var data = JsonConvert.DeserializeObject<Dictionary<string, object>>(options);
|
||||
Logger.Info(JsonConvert.SerializeObject(data));
|
||||
var result = await Execute(data);
|
||||
return System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
status = result.Item1,
|
||||
message = result.Item2
|
||||
});
|
||||
}
|
||||
|
||||
#pragma warning disable CS4014
|
||||
|
||||
public async void Execute(IDictionary<string, object> options, IJavascriptCallback callback)
|
||||
public async Task<Tuple<int, string>> Execute(IDictionary<string, object> options)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: switch to HttpClient
|
||||
#pragma warning disable SYSLIB0014 // Type or member is obsolete
|
||||
var request = WebRequest.CreateHttp((string)options["url"]);
|
||||
#pragma warning restore SYSLIB0014 // Type or member is obsolete
|
||||
if (ProxySet)
|
||||
request.Proxy = Proxy;
|
||||
|
||||
|
||||
request.CookieContainer = _cookieContainer;
|
||||
request.KeepAlive = true;
|
||||
request.UserAgent = Program.Version;
|
||||
request.AutomaticDecompression = DecompressionMethods.All;
|
||||
|
||||
if (options.TryGetValue("headers", out object headers))
|
||||
if (options.TryGetValue("headers", out var headers))
|
||||
{
|
||||
foreach (var header in (IEnumerable<KeyValuePair<string, object>>)headers)
|
||||
Dictionary<string, string> headersDict;
|
||||
if (headers.GetType() == typeof(JObject))
|
||||
{
|
||||
var key = header.Key;
|
||||
var value = header.Value.ToString();
|
||||
headersDict = ((JObject)headers).ToObject<Dictionary<string, string>>();
|
||||
}
|
||||
else
|
||||
{
|
||||
var headersKvp = (IEnumerable<KeyValuePair<string, object>>)headers;
|
||||
headersDict = new Dictionary<string, string>();
|
||||
foreach (var (key, value) in headersKvp)
|
||||
headersDict.Add(key, value.ToString());
|
||||
}
|
||||
|
||||
foreach (var (key, value) in headersDict)
|
||||
{
|
||||
if (string.Compare(key, "Content-Type", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
request.ContentType = value;
|
||||
}
|
||||
else if (string.Compare(key, "Referer", StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
request.Referer = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
request.Headers.Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options.TryGetValue("method", out object method))
|
||||
if (options.TryGetValue("method", out var method))
|
||||
{
|
||||
var _method = (string)method;
|
||||
request.Method = _method;
|
||||
|
||||
if (string.Compare(_method, "GET", StringComparison.OrdinalIgnoreCase) != 0 &&
|
||||
options.TryGetValue("body", out object body) == true)
|
||||
request.Method = (string)method;
|
||||
if (string.Compare(request.Method, "GET", StringComparison.OrdinalIgnoreCase) != 0 &&
|
||||
options.TryGetValue("body", out var body))
|
||||
{
|
||||
using (var stream = await request.GetRequestStreamAsync())
|
||||
using (var streamWriter = new StreamWriter(stream))
|
||||
{
|
||||
await streamWriter.WriteAsync((string)body);
|
||||
}
|
||||
await using var bodyStream = await request.GetRequestStreamAsync();
|
||||
await using var streamWriter = new StreamWriter(bodyStream);
|
||||
await streamWriter.WriteAsync((string)body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (options.TryGetValue("uploadImage", out _))
|
||||
{
|
||||
await ImageUpload(request, options);
|
||||
}
|
||||
|
||||
|
||||
if (options.TryGetValue("uploadFilePUT", out _))
|
||||
{
|
||||
await UploadFilePut(request, options);
|
||||
}
|
||||
|
||||
if (options.TryGetValue("uploadImageLegacy", out _))
|
||||
{
|
||||
await LegacyImageUpload(request, options);
|
||||
}
|
||||
|
||||
|
||||
if (options.TryGetValue("uploadImagePrint", out _))
|
||||
{
|
||||
await PrintImageUpload(request, options);
|
||||
|
||||
using var response = await request.GetResponseAsync() as HttpWebResponse;
|
||||
if (response?.Headers["Set-Cookie"] != null)
|
||||
_cookieDirty = true;
|
||||
|
||||
await using var imageStream = response.GetResponseStream();
|
||||
using var streamReader = new StreamReader(imageStream);
|
||||
if (response.ContentType.Contains("image/") ||
|
||||
response.ContentType.Contains("application/octet-stream"))
|
||||
{
|
||||
// base64 response data for image
|
||||
using var memoryStream = new MemoryStream();
|
||||
await imageStream.CopyToAsync(memoryStream);
|
||||
return new Tuple<int, string>(
|
||||
(int)response.StatusCode,
|
||||
$"data:image/png;base64,{Convert.ToBase64String(memoryStream.ToArray())}"
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
return new Tuple<int, string>(
|
||||
(int)response.StatusCode,
|
||||
await streamReader.ReadToEndAsync()
|
||||
);
|
||||
}
|
||||
catch (WebException webException)
|
||||
{
|
||||
if (webException.Response is HttpWebResponse response)
|
||||
{
|
||||
using (var response = await request.GetResponseAsync() as HttpWebResponse)
|
||||
{
|
||||
if (response.Headers["Set-Cookie"] != null)
|
||||
{
|
||||
_cookieDirty = true;
|
||||
}
|
||||
using (var stream = response.GetResponseStream())
|
||||
using (var streamReader = new StreamReader(stream))
|
||||
{
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
if (response.ContentType.Contains("image/") || response.ContentType.Contains("application/octet-stream"))
|
||||
{
|
||||
// base64 response data for image
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
await stream.CopyToAsync(memoryStream);
|
||||
callback.ExecuteAsync(null, new
|
||||
{
|
||||
data = $"data:image/png;base64,{Convert.ToBase64String(memoryStream.ToArray())}",
|
||||
status = response.StatusCode
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
callback.ExecuteAsync(null, new
|
||||
{
|
||||
data = await streamReader.ReadToEndAsync(),
|
||||
status = response.StatusCode
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebException webException)
|
||||
{
|
||||
if (webException.Response is HttpWebResponse response)
|
||||
{
|
||||
if (response.Headers["Set-Cookie"] != null)
|
||||
{
|
||||
_cookieDirty = true;
|
||||
}
|
||||
using (var stream = response.GetResponseStream())
|
||||
using (var streamReader = new StreamReader(stream))
|
||||
{
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(null, new
|
||||
{
|
||||
data = await streamReader.ReadToEndAsync(),
|
||||
status = response.StatusCode
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (callback.CanExecute == true)
|
||||
{
|
||||
callback.ExecuteAsync(webException.Message, null);
|
||||
}
|
||||
if (response.Headers["Set-Cookie"] != null)
|
||||
_cookieDirty = true;
|
||||
|
||||
await using var stream = response.GetResponseStream();
|
||||
using var streamReader = new StreamReader(stream);
|
||||
return new Tuple<int, string>(
|
||||
(int)response.StatusCode,
|
||||
await streamReader.ReadToEndAsync()
|
||||
);
|
||||
}
|
||||
|
||||
return new Tuple<int, string>(
|
||||
-1,
|
||||
webException.Message
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (callback.CanExecute == true)
|
||||
{
|
||||
// FIXME: 브라우저는 종료되었는데 얘는 이후에 실행되면 터짐
|
||||
callback.ExecuteAsync(e.Message, null);
|
||||
}
|
||||
return new Tuple<int, string>(
|
||||
-1,
|
||||
e.Message
|
||||
);
|
||||
}
|
||||
|
||||
callback.Dispose();
|
||||
}
|
||||
|
||||
#pragma warning restore CS4014
|
||||
}
|
||||
}
|
||||
5
Dotnet/global.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "8.0.404"
|
||||
}
|
||||
}
|
||||
BIN
Dotnet/libs/linux/SQLite.Interop.dll
Normal file
BIN
Dotnet/libs/linux/System.Data.SQLite.dll
Normal file
@@ -161,7 +161,7 @@ Section "Install" SecInstall
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
File /r /x *.log /x *.pdb "..\bin\x64\Release\*.*"
|
||||
File /r /x *.log /x *.pdb "..\build\Cef\*.*"
|
||||
|
||||
WriteRegStr HKLM "Software\VRCX" "InstallDir" $INSTDIR
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
### Download and run AppImage from [Releases](https://github.com/vrcx-team/VRCX/releases)
|
||||
|
||||
---
|
||||
|
||||
### Legacy Wine guide
|
||||
|
||||
VRCX on Linux was made possible by these people:
|
||||
|
||||
@RinLovesYou, @galister, @BenjaminZehowlt, @regalialong
|
||||
|
||||
8
VRCX.sln
@@ -3,13 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34309.116
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VRCX", "VRCX.csproj", "{D9F66F2E-3ED9-4D53-A6AC-ADCC1513562A}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VRCX-Cef", "Dotnet\VRCX-Cef.csproj", "{D9F66F2E-3ED9-4D53-A6AC-ADCC1513562A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8612F19B-3C1F-4B17-8679-A2747A53EC6B}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VRCX-Electron", "Dotnet\VRCX-Electron.csproj", "{B0275E4A-FE0F-410A-96F1-1D73DF2535FA}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|x64 = Debug|x64
|
||||
@@ -20,6 +22,10 @@ Global
|
||||
{D9F66F2E-3ED9-4D53-A6AC-ADCC1513562A}.Debug|x64.Build.0 = Debug|x64
|
||||
{D9F66F2E-3ED9-4D53-A6AC-ADCC1513562A}.Release|x64.ActiveCfg = Release|x64
|
||||
{D9F66F2E-3ED9-4D53-A6AC-ADCC1513562A}.Release|x64.Build.0 = Release|x64
|
||||
{B0275E4A-FE0F-410A-96F1-1D73DF2535FA}.Debug|x64.ActiveCfg = Debug|x64
|
||||
{B0275E4A-FE0F-410A-96F1-1D73DF2535FA}.Debug|x64.Build.0 = Debug|x64
|
||||
{B0275E4A-FE0F-410A-96F1-1D73DF2535FA}.Release|x64.ActiveCfg = Release|x64
|
||||
{B0275E4A-FE0F-410A-96F1-1D73DF2535FA}.Release|x64.Build.0 = Release|x64
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -11,22 +11,20 @@ $ZipName = "VRCX_" + $Date + ".zip"
|
||||
$SetupName = "VRCX_" + $Date + "_Setup.exe"
|
||||
|
||||
Write-Host "Building .Net..." -ForegroundColor Green
|
||||
dotnet build VRCX.sln -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
|
||||
dotnet build Dotnet\VRCX-Cef.csproj -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
|
||||
|
||||
Write-Host "Building Node.js..." -ForegroundColor Green
|
||||
cd "html"
|
||||
Remove-Item -Path "node_modules" -Force -Recurse -ErrorAction SilentlyContinue
|
||||
npm ci --loglevel=error
|
||||
npm run prod
|
||||
cd ..
|
||||
Remove-Item -Path "bin\x64\Release\html" -Force -Recurse -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Junction -Path "bin\x64\Release\html" -Target "html\dist"
|
||||
Remove-Item -Path "build\Cef\html" -Force -Recurse -ErrorAction SilentlyContinue
|
||||
New-Item -ItemType Junction -Path "build\Cef\html" -Target "html"
|
||||
|
||||
Write-Host "Creating Zip..." -ForegroundColor Green
|
||||
cd "bin\x64\Release"
|
||||
cd "build\Cef"
|
||||
7z a -tzip $ZipName * -mx=7 -xr0!"*.log" -xr0!"*.pdb"
|
||||
Move-Item $ZipName ..\..\..\$ZipName -Force
|
||||
cd ..\..\..\
|
||||
Move-Item $ZipName ..\..\$ZipName -Force
|
||||
cd ..\..\
|
||||
|
||||
Write-Host "Creating Installer..." -ForegroundColor Green
|
||||
cd "Installer"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@echo off
|
||||
cd ..
|
||||
dotnet build VRCX.sln -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
|
||||
mklink /J "%~dp0\..\bin\x64\Release\html" "%~dp0\..\html\dist"
|
||||
dotnet build Dotnet\VRCX-Cef.csproj -p:Configuration=Release -p:Platform=x64 -p:RestorePackagesConfig=true -t:"Restore;Clean;Build" -m --self-contained
|
||||
mklink /J "%~dp0\..\build\Cef\html" "%~dp0\..\build\html"
|
||||
pause
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
cd ../html
|
||||
cd ../src
|
||||
call npm ci
|
||||
call npm run production
|
||||
call npm run prod
|
||||
cd ..
|
||||
pause
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
mklink /J "%~dp0\..\bin\x64\Debug\html" "%~dp0\..\html\dist"
|
||||
mklink /J "%~dp0\..\bin\x64\Release\html" "%~dp0\..\html\dist"
|
||||
mklink /J "%~dp0\..\build\Cef\html" "%~dp0\..\build\html"
|
||||
pause
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
@echo off
|
||||
cd ..
|
||||
setlocal
|
||||
where /q 7z
|
||||
IF ERRORLEVEL 1 (
|
||||
set ZIP_BIN="C:\Program Files\7-Zip\7z.exe"
|
||||
) ELSE (
|
||||
set ZIP_BIN=7z
|
||||
)
|
||||
for /f %%a in ('powershell -Command "Get-Date -format yyyyMMdd"') do set TODAY=%%a
|
||||
set ZIP_NAME=VRCX_%TODAY%.zip
|
||||
echo %ZIP_NAME%
|
||||
rem using 7-Zip (https://www.7-zip.org)
|
||||
cd "%~dp0\..\bin\x64\Release"
|
||||
%ZIP_BIN% a -tzip %ZIP_NAME% * -mx=7 -xr0!*.log -xr0!*.pdb
|
||||
cd "%~dp0"
|
||||
move "%~dp0\..\bin\x64\Release\%ZIP_NAME%" "%~dp0"
|
||||
pause
|
||||
@@ -1,12 +1,18 @@
|
||||
@echo off
|
||||
cd ..
|
||||
setlocal
|
||||
where /q 7z
|
||||
IF ERRORLEVEL 1 (
|
||||
set ZIP_BIN="C:\Program Files\7-Zip\7z.exe"
|
||||
) ELSE (
|
||||
set ZIP_BIN=7z
|
||||
)
|
||||
for /f %%a in ('powershell -Command "Get-Date -format yyyyMMdd"') do set TODAY=%%a
|
||||
set ZIP_NAME=VRCX_%TODAY%.zip
|
||||
echo %ZIP_NAME%
|
||||
rem using bandizip (https://www.bandisoft.com/bandizip)
|
||||
cd "%~dp0\..\bin\x64\Release"
|
||||
bz c -l:9 -r -storeroot:yes -ex:"*.log;*.pdb" -cmt:"https://github.com/vrcx-team/VRCX" %ZIP_NAME% *
|
||||
rem using 7-Zip (https://www.7-zip.org)
|
||||
cd "%~dp0\..\build\Cef"
|
||||
%ZIP_BIN% a -tzip %ZIP_NAME% * -mx=7 -xr0!*.log -xr0!*.pdb
|
||||
cd "%~dp0"
|
||||
move "%~dp0\..\bin\x64\Release\%ZIP_NAME%" "%~dp0"
|
||||
move "%~dp0\..\build\Cef\%ZIP_NAME%" "%~dp0"
|
||||
pause
|
||||
|
||||
2
html/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules/
|
||||
dist/
|
||||
6708
html/package-lock.json
generated
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "webpack --config webpack.config.js --mode development",
|
||||
"watch": "webpack --config webpack.config.js --mode development --watch",
|
||||
"prod": "webpack --config webpack.config.js --mode production",
|
||||
"lint": "eslint --ext .js src/ && prettier --check src/",
|
||||
"development": "npm run dev",
|
||||
"production": "npm run prod",
|
||||
"localization": "node ./src/localization/localizationHelperCLI.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vrcx-team/VRCX.git"
|
||||
},
|
||||
"keywords": [
|
||||
"vrchat"
|
||||
],
|
||||
"author": "pypy <git@pypy.moe>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vrcx-team/VRCX/issues"
|
||||
},
|
||||
"homepage": "https://github.com/vrcx-team/VRCX#readme",
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.25.9",
|
||||
"@babel/plugin-syntax-import-assertions": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@fontsource/noto-sans-jp": "^5.1.0",
|
||||
"@fontsource/noto-sans-kr": "^5.1.0",
|
||||
"@fontsource/noto-sans-sc": "^5.1.0",
|
||||
"@fontsource/noto-sans-tc": "^5.1.0",
|
||||
"@infolektuell/noto-color-emoji": "^0.2.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^7.1.2",
|
||||
"default-passive-events": "^2.0.0",
|
||||
"element-ui": "^2.15.14",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"html-webpack-plugin": "^5.6.3",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"normalize.css": "^8.0.1",
|
||||
"noty": "^3.2.0-beta-deprecated",
|
||||
"prettier": "^3.3.3",
|
||||
"pug": "^3.0.3",
|
||||
"pug-plain-loader": "^1.1.0",
|
||||
"raw-loader": "^4.0.2",
|
||||
"sass": "^1.81.0",
|
||||
"sass-loader": "^16.0.3",
|
||||
"vue": "^2.6.14",
|
||||
"vue-data-tables": "^3.4.5",
|
||||
"vue-i18n": "^8.28.2",
|
||||
"vue-lazyload": "^1.3.4",
|
||||
"vue-loader": "^15.11.1",
|
||||
"vue-markdown": "^2.2.4",
|
||||
"vue-marquee-text-component": "^1.2.0",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"worker-timers": "^8.0.11",
|
||||
"yargs": "^17.7.2"
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
mixin vrcx()
|
||||
//- dialog: Cache Download
|
||||
el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="downloadDialog" :visible.sync="downloadDialog.visible" :title="$t('dialog.download_history.header')" width="770px")
|
||||
template(v-if="downloadDialog.visible")
|
||||
div(v-if="downloadInProgress && downloadCurrent.ref")
|
||||
span(v-text="downloadCurrent.ref.name")
|
||||
el-button(type="text" icon="el-icon-close" size="mini" @click="cancelDownload(downloadCurrent.id)" style="margin-left:5px")
|
||||
el-progress(:percentage="downloadProgress" :format="downloadProgressText")
|
||||
template(v-if="downloadQueueTable.data.length >= 1")
|
||||
span(style="margin-top:15px") {{ $t('dialog.download_history.queue') }}
|
||||
data-tables(v-bind="downloadQueueTable" style="margin-top:10px")
|
||||
el-table-column(:label="$t('table.download_history.name')" prop="name")
|
||||
el-table-column(:label="$t('table.download_history.type')" prop="type" width="70")
|
||||
el-table-column(:label="$t('table.download_history.cancel')" width="60" align="right")
|
||||
template(v-once #default="scope")
|
||||
el-button(type="text" icon="el-icon-close" size="mini" @click="cancelDownload(scope.row.ref.id)")
|
||||
span(style="margin-top:15px") {{ $t('dialog.download_history.history') }}
|
||||
data-tables(v-bind="downloadHistoryTable" style="margin-top:10px")
|
||||
el-table-column(:label="$t('table.download_history.time')" prop="date" width="90")
|
||||
template(v-once #default="scope")
|
||||
timer(:epoch="scope.row.date")
|
||||
el-table-column(:label="$t('table.download_history.name')" prop="name")
|
||||
template(v-once #default="scope")
|
||||
span(v-text="scope.row.ref.name")
|
||||
el-table-column(:label="$t('table.download_history.type')" prop="type" width="70")
|
||||
el-table-column(:label="$t('table.download_history.status')" prop="status" width="80")
|
||||
template(#footer)
|
||||
el-button(v-if="downloadQueue.size >= 1" size="small" @click="cancelAllDownloads") {{ $t('dialog.download_history.cancel_all') }}
|
||||
el-button(size="small" @click="downloadDialog.visible = false") {{ $t('dialog.download_history.close') }}
|
||||
|
||||
//- dialog: update VRCX
|
||||
el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="VRCXUpdateDialog" :visible.sync="VRCXUpdateDialog.visible" :title="$t('dialog.vrcx_updater.header')" width="400px")
|
||||
div(v-loading="checkingForVRCXUpdate" style="margin-top:15px")
|
||||
div(v-if="VRCXUpdateDialog.updatePending" style="margin-bottom:15px")
|
||||
span(v-text="pendingVRCXInstall")
|
||||
br
|
||||
span {{ $t('dialog.vrcx_updater.ready_for_update') }}
|
||||
el-select(v-model="branch" @change="loadBranchVersions" style="display:inline-block;width:150px;margin-right:15px")
|
||||
el-option(v-once v-for="branch in branches" :key="branch.name" :label="branch.name" :value="branch.name")
|
||||
el-select(v-model="VRCXUpdateDialog.release" style="display:inline-block;width:150px")
|
||||
el-option(v-for="item in VRCXUpdateDialog.releases" :key="item.name" :label="item.tag_name" :value="item.name")
|
||||
div(v-if="!VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release === appVersion" style="margin-top:15px")
|
||||
span {{ $t('dialog.vrcx_updater.latest_version') }}
|
||||
template(#footer)
|
||||
el-button(v-if="(VRCXUpdateDialog.updatePending && VRCXUpdateDialog.release !== pendingVRCXInstall) || VRCXUpdateDialog.release !== appVersion" type="primary" size="small" @click="installVRCXUpdate") {{ $t('dialog.vrcx_updater.download') }}
|
||||
el-button(v-if="VRCXUpdateDialog.updatePending" type="primary" size="small" @click="restartVRCX(true)") {{ $t('dialog.vrcx_updater.install') }}
|
||||
|
||||
//- dialog: change log
|
||||
el-dialog.x-dialog(:before-close="beforeDialogClose" @mousedown.native="dialogMouseDown" @mouseup.native="dialogMouseUp" ref="changeLogDialog" :visible.sync="changeLogDialog.visible" :title="$t('dialog.change_log.header')" width="800px" top="5vh")
|
||||
.changelog-dialog(v-if="changeLogDialog.visible")
|
||||
h2(v-text="changeLogDialog.buildName")
|
||||
span {{ $t('dialog.change_log.description') }} #[a.x-link(@click="openExternalLink('https://www.patreon.com/Natsumi_VRCX')") Patreon], #[a.x-link(@click="openExternalLink('https://ko-fi.com/natsumi_sama')") Ko-fi].
|
||||
vue-markdown(:source="changeLogDialog.changeLog" :linkify="false" style="height:62vh;overflow-y:auto;margin-top:10px")
|
||||
template(#footer)
|
||||
el-button(type="small" @click="openExternalLink('https://github.com/vrcx-team/VRCX/releases')") {{ $t('dialog.change_log.github') }}
|
||||
el-button(type="small" @click="openExternalLink('https://patreon.com/Natsumi_VRCX')") {{ $t('dialog.change_log.donate') }}
|
||||
el-button(type="small" @click="changeLogDialog.visible = false") {{ $t('dialog.change_log.close') }}
|
||||
@@ -1,30 +0,0 @@
|
||||
// requires binding of SQLite
|
||||
|
||||
class SQLiteService {
|
||||
execute(callback, sql, args = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
SQLite.Execute(
|
||||
(err, data) => {
|
||||
if (err !== null) {
|
||||
reject(err);
|
||||
} else if (data === null) {
|
||||
resolve();
|
||||
} else {
|
||||
callback(data);
|
||||
}
|
||||
},
|
||||
sql,
|
||||
args
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
executeNonQuery(sql, args = null) {
|
||||
return SQLite.ExecuteNonQuery(sql, args);
|
||||
}
|
||||
}
|
||||
|
||||
var self = new SQLiteService();
|
||||
window.sqliteService = self;
|
||||
|
||||
export { self as default, SQLiteService };
|
||||
@@ -1,32 +0,0 @@
|
||||
// requires binding of WebApi
|
||||
|
||||
class WebApiService {
|
||||
clearCookies() {
|
||||
return WebApi.ClearCookies();
|
||||
}
|
||||
|
||||
getCookies() {
|
||||
return WebApi.GetCookies();
|
||||
}
|
||||
|
||||
setCookies(cookie) {
|
||||
return WebApi.SetCookies(cookie);
|
||||
}
|
||||
|
||||
execute(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
WebApi.Execute(options, (err, response) => {
|
||||
if (err !== null) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var self = new WebApiService();
|
||||
window.webApiService = self;
|
||||
|
||||
export { self as default, WebApiService };
|
||||
|
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 557 B |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 903 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |