mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-18 06:13:52 +02:00
Open instance in-game
This commit is contained in:
@@ -5,6 +5,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using librsync.net;
|
||||
using Newtonsoft.Json;
|
||||
using NLog;
|
||||
@@ -165,5 +166,10 @@ namespace VRCX
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Task<bool> TryOpenInstanceInVrc(string launchUrl)
|
||||
{
|
||||
return VRCIPC.Send(launchUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Dotnet/IPC/VRCIPC.cs
Normal file
43
Dotnet/IPC/VRCIPC.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace VRCX;
|
||||
|
||||
public class VRCIPC
|
||||
{
|
||||
private const string PipeName = "VRChatURLLaunchPipe";
|
||||
private static NamedPipeClientStream _ipcClient;
|
||||
|
||||
private static void TryConnect()
|
||||
{
|
||||
if (_ipcClient != null && _ipcClient.IsConnected)
|
||||
return;
|
||||
|
||||
_ipcClient = new NamedPipeClientStream(".", PipeName, PipeDirection.InOut);
|
||||
_ipcClient.Connect(1000);
|
||||
}
|
||||
|
||||
public static async Task<bool> Send(string message)
|
||||
{
|
||||
TryConnect();
|
||||
if (_ipcClient == null || !_ipcClient.IsConnected)
|
||||
{
|
||||
Dispose();
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(message);
|
||||
_ipcClient.Write(bytes, 0, bytes.Length);
|
||||
var result = new byte[1];
|
||||
await _ipcClient.ReadExactlyAsync(result, 0, 1);
|
||||
Dispose();
|
||||
return result[0] == 1;
|
||||
}
|
||||
|
||||
private static void Dispose()
|
||||
{
|
||||
_ipcClient?.Close();
|
||||
_ipcClient = null;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,51 @@
|
||||
<template>
|
||||
<el-button v-show="isVisible" @click="confirmInvite" size="mini" icon="el-icon-message" circle />
|
||||
<el-tooltip
|
||||
v-if="!isGameRunning || isLinux || gameLogDisabled"
|
||||
placement="top"
|
||||
:content="t('dialog.user.info.self_invite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<el-button v-show="isVisible" @click="confirmInvite" size="mini" icon="el-icon-message" circle />
|
||||
</el-tooltip>
|
||||
<el-tooltip v-else placement="top" :content="t('dialog.user.info.open_in_vrchat_tooltip')" :disabled="hideTooltips">
|
||||
<el-button @click="openInstance" size="mini" icon="el-icon-message" circle />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed, getCurrentInstance } from 'vue';
|
||||
import { useI18n } from 'vue-i18n-bridge';
|
||||
import { instanceRequest } from '../api';
|
||||
import { checkCanInviteSelf, parseLocation } from '../shared/utils';
|
||||
import { useAppearanceSettingsStore } from '../stores/settings/appearance';
|
||||
import { useGameStore } from '../stores/game';
|
||||
import { useLaunchStore } from '../stores/launch';
|
||||
import { useAdvancedSettingsStore } from '../stores/settings/advanced';
|
||||
|
||||
const props = defineProps({
|
||||
location: String,
|
||||
shortname: String
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
const { gameLogDisabled } = storeToRefs(useAdvancedSettingsStore());
|
||||
|
||||
const { tryOpenInstanceInVrc } = useLaunchStore();
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const isVisible = computed(() => checkCanInviteSelf(props.location));
|
||||
|
||||
const isLinux = computed(() => LINUX);
|
||||
|
||||
function confirmInvite() {
|
||||
const L = parseLocation(props.location);
|
||||
if (!L.isRealInstance) return;
|
||||
if (!L.isRealInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instanceRequest
|
||||
.selfInvite({
|
||||
@@ -31,4 +58,13 @@
|
||||
return args;
|
||||
});
|
||||
}
|
||||
|
||||
function openInstance() {
|
||||
const L = parseLocation(props.location);
|
||||
if (!L.isRealInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
tryOpenInstanceInVrc(L.tag, props.shortname);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -413,9 +413,7 @@
|
||||
<div v-for="room in groupDialog.instances" :key="room.tag" style="width: 100%">
|
||||
<div style="margin: 5px 0">
|
||||
<Location :location="room.tag" style="display: inline-block" />
|
||||
<el-tooltip placement="top" content="Invite yourself" :disabled="hideTooltips">
|
||||
<InviteYourself :location="room.tag" style="margin-left: 5px" />
|
||||
</el-tooltip>
|
||||
<InviteYourself :location="room.tag" style="margin-left: 5px" />
|
||||
<el-tooltip placement="top" content="Refresh player count" :disabled="hideTooltips">
|
||||
<el-button
|
||||
size="mini"
|
||||
|
||||
@@ -66,13 +66,31 @@
|
||||
@click="showInviteDialog(launchDialog.location)">
|
||||
{{ t('dialog.launch.invite') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!launchDialog.secureOrShortName"
|
||||
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
|
||||
{{ t('dialog.launch.launch') }}
|
||||
</el-button>
|
||||
<template v-if="isGameRunning">
|
||||
<el-button
|
||||
type="default"
|
||||
size="small"
|
||||
:disabled="!launchDialog.secureOrShortName"
|
||||
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
|
||||
{{ t('dialog.launch.launch') }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!launchDialog.secureOrShortName"
|
||||
@click="handleAttachGame(launchDialog.location, launchDialog.shortName)">
|
||||
{{ t('dialog.launch.open_ingame') }}
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="!launchDialog.secureOrShortName"
|
||||
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
|
||||
{{ t('dialog.launch.launch') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
|
||||
</safe-dialog>
|
||||
@@ -88,6 +106,7 @@
|
||||
import {
|
||||
useAppearanceSettingsStore,
|
||||
useFriendStore,
|
||||
useGameStore,
|
||||
useInstanceStore,
|
||||
useLaunchStore,
|
||||
useLocationStore
|
||||
@@ -100,9 +119,10 @@
|
||||
const { friends } = storeToRefs(useFriendStore());
|
||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||
const { lastLocation } = storeToRefs(useLocationStore());
|
||||
const { launchGame } = useLaunchStore();
|
||||
const { launchGame, tryOpenInstanceInVrc } = useLaunchStore();
|
||||
const { launchDialogData } = storeToRefs(useLaunchStore());
|
||||
const { showPreviousInstancesInfoDialog } = useInstanceStore();
|
||||
const { isGameRunning } = storeToRefs(useGameStore());
|
||||
|
||||
const launchDialogRef = ref(null);
|
||||
|
||||
@@ -180,6 +200,10 @@
|
||||
launchGame(location, shortName, desktop);
|
||||
isVisible.value = false;
|
||||
}
|
||||
function handleAttachGame(location, shortName) {
|
||||
tryOpenInstanceInVrc(location, shortName);
|
||||
isVisible.value = false;
|
||||
}
|
||||
function getConfig() {
|
||||
configRepository.getBool('launchAsDesktop').then((value) => (launchDialog.value.desktop = value));
|
||||
}
|
||||
|
||||
@@ -568,15 +568,10 @@
|
||||
<div style="flex: none">
|
||||
<template v-if="isRealInstance(userDialog.$location.tag)">
|
||||
<Launch :location="userDialog.$location.tag" />
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="t('dialog.user.info.self_invite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<InviteYourself
|
||||
:location="userDialog.$location.tag"
|
||||
:shortname="userDialog.$location.shortName"
|
||||
style="margin-left: 5px" />
|
||||
</el-tooltip>
|
||||
<InviteYourself
|
||||
:location="userDialog.$location.tag"
|
||||
:shortname="userDialog.$location.shortName"
|
||||
style="margin-left: 5px" />
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="t('dialog.user.info.refresh_instance_info')"
|
||||
|
||||
@@ -348,15 +348,10 @@
|
||||
:currentuserid="currentUser.id"
|
||||
:worlddialogshortname="worldDialog.$location.shortName" />
|
||||
<Launch :location="room.tag" style="margin-left: 5px" />
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="t('dialog.world.instances.self_invite_tooltip')"
|
||||
:disabled="hideTooltips">
|
||||
<InviteYourself
|
||||
:location="room.$location.tag"
|
||||
:shortname="room.$location.shortName"
|
||||
style="margin-left: 5px" />
|
||||
</el-tooltip>
|
||||
<InviteYourself
|
||||
:location="room.$location.tag"
|
||||
:shortname="room.$location.shortName"
|
||||
style="margin-left: 5px" />
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
:content="t('dialog.world.instances.refresh_instance_info')"
|
||||
|
||||
@@ -525,7 +525,7 @@
|
||||
"show_images": "Show world images",
|
||||
"show_current_platform": "Show current platform",
|
||||
"world_integration": "World integration",
|
||||
"world_integration_tooltip": "Show \"Watching\\Listening to\" video for Popcorn Palace, PyPyDance, VRDancing and LS Media",
|
||||
"world_integration_tooltip": "Show \"Watching/Listening to\" for Popcorn Palace, PyPyDance, VRDancing and LS Media",
|
||||
"display_world_name_as_discord_status": "Display world name as Discord status"
|
||||
},
|
||||
"rpc": {
|
||||
@@ -785,6 +785,7 @@
|
||||
"header": "Info",
|
||||
"launch_invite_tooltip": "Launch/Invite",
|
||||
"self_invite_tooltip": "Invite Yourself",
|
||||
"open_in_vrchat_tooltip": "Open in VRChat",
|
||||
"refresh_instance_info": "Refresh Instance Info",
|
||||
"instance_queue": "Queue:",
|
||||
"instance_users": "Users:",
|
||||
@@ -1321,7 +1322,8 @@
|
||||
"start_as_desktop": "Start as Desktop (No VR)",
|
||||
"info": "Info",
|
||||
"invite": "Invite",
|
||||
"launch": "Launch"
|
||||
"launch": "Launch",
|
||||
"open_ingame": "Open in Game"
|
||||
},
|
||||
"export_friends_list": {
|
||||
"header": "Export Friends List",
|
||||
|
||||
@@ -64,50 +64,87 @@ export const useLaunchStore = defineStore('Launch', () => {
|
||||
*
|
||||
* @param {string} location
|
||||
* @param {string} shortName
|
||||
* @param {boolean} desktopMode
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<string>} launchUrl
|
||||
*/
|
||||
async function launchGame(location, shortName, desktopMode) {
|
||||
async function getLaunchUrl(location, shortName) {
|
||||
const L = parseLocation(location);
|
||||
const args = [];
|
||||
if (
|
||||
shortName &&
|
||||
L.instanceType !== 'public' &&
|
||||
L.groupAccessType !== 'public'
|
||||
) {
|
||||
args.push(
|
||||
`vrchat://launch?ref=vrcx.app&id=${location}&shortName=${shortName}`
|
||||
);
|
||||
} else {
|
||||
// fetch shortName
|
||||
let newShortName = '';
|
||||
const response = await instanceRequest.getInstanceShortName({
|
||||
worldId: L.worldId,
|
||||
instanceId: L.instanceId
|
||||
});
|
||||
if (response.json) {
|
||||
if (response.json.shortName) {
|
||||
newShortName = response.json.shortName;
|
||||
} else {
|
||||
newShortName = response.json.secureName;
|
||||
}
|
||||
}
|
||||
if (newShortName) {
|
||||
args.push(
|
||||
`vrchat://launch?ref=vrcx.app&id=${location}&shortName=${newShortName}`
|
||||
);
|
||||
} else {
|
||||
args.push(`vrchat://launch?ref=vrcx.app&id=${location}`);
|
||||
}
|
||||
return `vrchat://launch?ref=vrcx.app&id=${location}&shortName=${shortName}`;
|
||||
}
|
||||
|
||||
// fetch shortName
|
||||
let newShortName = '';
|
||||
const response = await instanceRequest.getInstanceShortName({
|
||||
worldId: L.worldId,
|
||||
instanceId: L.instanceId
|
||||
});
|
||||
if (response.json) {
|
||||
if (response.json.shortName) {
|
||||
newShortName = response.json.shortName;
|
||||
} else {
|
||||
newShortName = response.json.secureName;
|
||||
}
|
||||
}
|
||||
if (newShortName) {
|
||||
return `vrchat://launch?ref=vrcx.app&id=${location}&shortName=${newShortName}`;
|
||||
}
|
||||
return `vrchat://launch?ref=vrcx.app&id=${location}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* launch.exe &attach=1
|
||||
* @param {string} location
|
||||
* @param {string} shortName
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function tryOpenInstanceInVrc(location, shortName) {
|
||||
const launchUrl = await getLaunchUrl(location, shortName);
|
||||
let result = false;
|
||||
try {
|
||||
result = await AppApi.TryOpenInstanceInVrc(launchUrl);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
console.log('Attach Game', launchUrl, result);
|
||||
if (!result) {
|
||||
$app.$message({
|
||||
message:
|
||||
'Failed open instance in VRChat, falling back to self invite',
|
||||
type: 'warning'
|
||||
});
|
||||
// self invite fallback
|
||||
const L = parseLocation(location);
|
||||
await instanceRequest.selfInvite({
|
||||
instanceId: L.instanceId,
|
||||
worldId: L.worldId,
|
||||
shortName
|
||||
});
|
||||
$app.$message({
|
||||
message: 'Self invite sent',
|
||||
type: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} location
|
||||
* @param {string} shortName
|
||||
* @param {boolean} desktopMode
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function launchGame(location, shortName, desktopMode) {
|
||||
const launchUrl = await getLaunchUrl(location, shortName);
|
||||
const args = [launchUrl];
|
||||
const launchArguments =
|
||||
await configRepository.getString('launchArguments');
|
||||
|
||||
const vrcLaunchPathOverride = await configRepository.getString(
|
||||
'vrcLaunchPathOverride'
|
||||
);
|
||||
|
||||
if (launchArguments) {
|
||||
args.push(launchArguments);
|
||||
}
|
||||
@@ -157,6 +194,7 @@ export const useLaunchStore = defineStore('Launch', () => {
|
||||
launchDialogData,
|
||||
showLaunchOptions,
|
||||
showLaunchDialog,
|
||||
launchGame
|
||||
launchGame,
|
||||
tryOpenInstanceInVrc
|
||||
};
|
||||
});
|
||||
|
||||
1
src/types/globals.d.ts
vendored
1
src/types/globals.d.ts
vendored
@@ -201,6 +201,7 @@ declare global {
|
||||
runProcessOnce: boolean
|
||||
): Promise<void>;
|
||||
GetFileBase64(path: string): Promise<string | null>;
|
||||
TryOpenInstanceInVrc(launchUrl: string): Promise<boolean>;
|
||||
|
||||
// Folders
|
||||
GetVRChatAppDataLocation(): Promise<string>;
|
||||
|
||||
@@ -1207,7 +1207,8 @@
|
||||
@change="
|
||||
setDiscordWorldIntegration();
|
||||
saveDiscordOption();
|
||||
" />
|
||||
"
|
||||
:tooltip="t('view.settings.discord_presence.discord_presence.world_integration_tooltip')" />
|
||||
<simple-switch
|
||||
:label="t('view.settings.discord_presence.discord_presence.instance_type_player_count')"
|
||||
:value="discordInstance"
|
||||
|
||||
Reference in New Issue
Block a user