mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 22:46:06 +02:00
Open instance in-game
This commit is contained in:
@@ -5,6 +5,7 @@ using System.Globalization;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using librsync.net;
|
using librsync.net;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using NLog;
|
using NLog;
|
||||||
@@ -165,5 +166,10 @@ namespace VRCX
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<bool> TryOpenInstanceInVrc(string launchUrl)
|
||||||
|
{
|
||||||
|
return VRCIPC.Send(launchUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
import { computed, getCurrentInstance } from 'vue';
|
import { computed, getCurrentInstance } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n-bridge';
|
||||||
import { instanceRequest } from '../api';
|
import { instanceRequest } from '../api';
|
||||||
import { checkCanInviteSelf, parseLocation } from '../shared/utils';
|
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({
|
const props = defineProps({
|
||||||
location: String,
|
location: String,
|
||||||
shortname: 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 { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
const isVisible = computed(() => checkCanInviteSelf(props.location));
|
const isVisible = computed(() => checkCanInviteSelf(props.location));
|
||||||
|
|
||||||
|
const isLinux = computed(() => LINUX);
|
||||||
|
|
||||||
function confirmInvite() {
|
function confirmInvite() {
|
||||||
const L = parseLocation(props.location);
|
const L = parseLocation(props.location);
|
||||||
if (!L.isRealInstance) return;
|
if (!L.isRealInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
instanceRequest
|
instanceRequest
|
||||||
.selfInvite({
|
.selfInvite({
|
||||||
@@ -31,4 +58,13 @@
|
|||||||
return args;
|
return args;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openInstance() {
|
||||||
|
const L = parseLocation(props.location);
|
||||||
|
if (!L.isRealInstance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryOpenInstanceInVrc(L.tag, props.shortname);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -413,9 +413,7 @@
|
|||||||
<div v-for="room in groupDialog.instances" :key="room.tag" style="width: 100%">
|
<div v-for="room in groupDialog.instances" :key="room.tag" style="width: 100%">
|
||||||
<div style="margin: 5px 0">
|
<div style="margin: 5px 0">
|
||||||
<Location :location="room.tag" style="display: inline-block" />
|
<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" />
|
||||||
<InviteYourself :location="room.tag" style="margin-left: 5px" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip placement="top" content="Refresh player count" :disabled="hideTooltips">
|
<el-tooltip placement="top" content="Refresh player count" :disabled="hideTooltips">
|
||||||
<el-button
|
<el-button
|
||||||
size="mini"
|
size="mini"
|
||||||
|
|||||||
@@ -66,13 +66,31 @@
|
|||||||
@click="showInviteDialog(launchDialog.location)">
|
@click="showInviteDialog(launchDialog.location)">
|
||||||
{{ t('dialog.launch.invite') }}
|
{{ t('dialog.launch.invite') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<template v-if="isGameRunning">
|
||||||
type="primary"
|
<el-button
|
||||||
size="small"
|
type="default"
|
||||||
:disabled="!launchDialog.secureOrShortName"
|
size="small"
|
||||||
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
|
:disabled="!launchDialog.secureOrShortName"
|
||||||
{{ t('dialog.launch.launch') }}
|
@click="handleLaunchGame(launchDialog.location, launchDialog.shortName, launchDialog.desktop)">
|
||||||
</el-button>
|
{{ 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>
|
</template>
|
||||||
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
|
<InviteDialog :invite-dialog="inviteDialog" @closeInviteDialog="closeInviteDialog" />
|
||||||
</safe-dialog>
|
</safe-dialog>
|
||||||
@@ -88,6 +106,7 @@
|
|||||||
import {
|
import {
|
||||||
useAppearanceSettingsStore,
|
useAppearanceSettingsStore,
|
||||||
useFriendStore,
|
useFriendStore,
|
||||||
|
useGameStore,
|
||||||
useInstanceStore,
|
useInstanceStore,
|
||||||
useLaunchStore,
|
useLaunchStore,
|
||||||
useLocationStore
|
useLocationStore
|
||||||
@@ -100,9 +119,10 @@
|
|||||||
const { friends } = storeToRefs(useFriendStore());
|
const { friends } = storeToRefs(useFriendStore());
|
||||||
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
const { hideTooltips } = storeToRefs(useAppearanceSettingsStore());
|
||||||
const { lastLocation } = storeToRefs(useLocationStore());
|
const { lastLocation } = storeToRefs(useLocationStore());
|
||||||
const { launchGame } = useLaunchStore();
|
const { launchGame, tryOpenInstanceInVrc } = useLaunchStore();
|
||||||
const { launchDialogData } = storeToRefs(useLaunchStore());
|
const { launchDialogData } = storeToRefs(useLaunchStore());
|
||||||
const { showPreviousInstancesInfoDialog } = useInstanceStore();
|
const { showPreviousInstancesInfoDialog } = useInstanceStore();
|
||||||
|
const { isGameRunning } = storeToRefs(useGameStore());
|
||||||
|
|
||||||
const launchDialogRef = ref(null);
|
const launchDialogRef = ref(null);
|
||||||
|
|
||||||
@@ -180,6 +200,10 @@
|
|||||||
launchGame(location, shortName, desktop);
|
launchGame(location, shortName, desktop);
|
||||||
isVisible.value = false;
|
isVisible.value = false;
|
||||||
}
|
}
|
||||||
|
function handleAttachGame(location, shortName) {
|
||||||
|
tryOpenInstanceInVrc(location, shortName);
|
||||||
|
isVisible.value = false;
|
||||||
|
}
|
||||||
function getConfig() {
|
function getConfig() {
|
||||||
configRepository.getBool('launchAsDesktop').then((value) => (launchDialog.value.desktop = value));
|
configRepository.getBool('launchAsDesktop').then((value) => (launchDialog.value.desktop = value));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -568,15 +568,10 @@
|
|||||||
<div style="flex: none">
|
<div style="flex: none">
|
||||||
<template v-if="isRealInstance(userDialog.$location.tag)">
|
<template v-if="isRealInstance(userDialog.$location.tag)">
|
||||||
<Launch :location="userDialog.$location.tag" />
|
<Launch :location="userDialog.$location.tag" />
|
||||||
<el-tooltip
|
<InviteYourself
|
||||||
placement="top"
|
:location="userDialog.$location.tag"
|
||||||
:content="t('dialog.user.info.self_invite_tooltip')"
|
:shortname="userDialog.$location.shortName"
|
||||||
:disabled="hideTooltips">
|
style="margin-left: 5px" />
|
||||||
<InviteYourself
|
|
||||||
:location="userDialog.$location.tag"
|
|
||||||
:shortname="userDialog.$location.shortName"
|
|
||||||
style="margin-left: 5px" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
:content="t('dialog.user.info.refresh_instance_info')"
|
:content="t('dialog.user.info.refresh_instance_info')"
|
||||||
|
|||||||
@@ -348,15 +348,10 @@
|
|||||||
:currentuserid="currentUser.id"
|
:currentuserid="currentUser.id"
|
||||||
:worlddialogshortname="worldDialog.$location.shortName" />
|
:worlddialogshortname="worldDialog.$location.shortName" />
|
||||||
<Launch :location="room.tag" style="margin-left: 5px" />
|
<Launch :location="room.tag" style="margin-left: 5px" />
|
||||||
<el-tooltip
|
<InviteYourself
|
||||||
placement="top"
|
:location="room.$location.tag"
|
||||||
:content="t('dialog.world.instances.self_invite_tooltip')"
|
:shortname="room.$location.shortName"
|
||||||
:disabled="hideTooltips">
|
style="margin-left: 5px" />
|
||||||
<InviteYourself
|
|
||||||
:location="room.$location.tag"
|
|
||||||
:shortname="room.$location.shortName"
|
|
||||||
style="margin-left: 5px" />
|
|
||||||
</el-tooltip>
|
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
placement="top"
|
placement="top"
|
||||||
:content="t('dialog.world.instances.refresh_instance_info')"
|
:content="t('dialog.world.instances.refresh_instance_info')"
|
||||||
|
|||||||
@@ -525,7 +525,7 @@
|
|||||||
"show_images": "Show world images",
|
"show_images": "Show world images",
|
||||||
"show_current_platform": "Show current platform",
|
"show_current_platform": "Show current platform",
|
||||||
"world_integration": "World integration",
|
"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"
|
"display_world_name_as_discord_status": "Display world name as Discord status"
|
||||||
},
|
},
|
||||||
"rpc": {
|
"rpc": {
|
||||||
@@ -785,6 +785,7 @@
|
|||||||
"header": "Info",
|
"header": "Info",
|
||||||
"launch_invite_tooltip": "Launch/Invite",
|
"launch_invite_tooltip": "Launch/Invite",
|
||||||
"self_invite_tooltip": "Invite Yourself",
|
"self_invite_tooltip": "Invite Yourself",
|
||||||
|
"open_in_vrchat_tooltip": "Open in VRChat",
|
||||||
"refresh_instance_info": "Refresh Instance Info",
|
"refresh_instance_info": "Refresh Instance Info",
|
||||||
"instance_queue": "Queue:",
|
"instance_queue": "Queue:",
|
||||||
"instance_users": "Users:",
|
"instance_users": "Users:",
|
||||||
@@ -1321,7 +1322,8 @@
|
|||||||
"start_as_desktop": "Start as Desktop (No VR)",
|
"start_as_desktop": "Start as Desktop (No VR)",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"invite": "Invite",
|
"invite": "Invite",
|
||||||
"launch": "Launch"
|
"launch": "Launch",
|
||||||
|
"open_ingame": "Open in Game"
|
||||||
},
|
},
|
||||||
"export_friends_list": {
|
"export_friends_list": {
|
||||||
"header": "Export Friends List",
|
"header": "Export Friends List",
|
||||||
|
|||||||
+69
-31
@@ -64,50 +64,87 @@ export const useLaunchStore = defineStore('Launch', () => {
|
|||||||
*
|
*
|
||||||
* @param {string} location
|
* @param {string} location
|
||||||
* @param {string} shortName
|
* @param {string} shortName
|
||||||
* @param {boolean} desktopMode
|
* @returns {Promise<string>} launchUrl
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
*/
|
||||||
async function launchGame(location, shortName, desktopMode) {
|
async function getLaunchUrl(location, shortName) {
|
||||||
const L = parseLocation(location);
|
const L = parseLocation(location);
|
||||||
const args = [];
|
|
||||||
if (
|
if (
|
||||||
shortName &&
|
shortName &&
|
||||||
L.instanceType !== 'public' &&
|
L.instanceType !== 'public' &&
|
||||||
L.groupAccessType !== 'public'
|
L.groupAccessType !== 'public'
|
||||||
) {
|
) {
|
||||||
args.push(
|
return `vrchat://launch?ref=vrcx.app&id=${location}&shortName=${shortName}`;
|
||||||
`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}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 =
|
const launchArguments =
|
||||||
await configRepository.getString('launchArguments');
|
await configRepository.getString('launchArguments');
|
||||||
|
|
||||||
const vrcLaunchPathOverride = await configRepository.getString(
|
const vrcLaunchPathOverride = await configRepository.getString(
|
||||||
'vrcLaunchPathOverride'
|
'vrcLaunchPathOverride'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (launchArguments) {
|
if (launchArguments) {
|
||||||
args.push(launchArguments);
|
args.push(launchArguments);
|
||||||
}
|
}
|
||||||
@@ -157,6 +194,7 @@ export const useLaunchStore = defineStore('Launch', () => {
|
|||||||
launchDialogData,
|
launchDialogData,
|
||||||
showLaunchOptions,
|
showLaunchOptions,
|
||||||
showLaunchDialog,
|
showLaunchDialog,
|
||||||
launchGame
|
launchGame,
|
||||||
|
tryOpenInstanceInVrc
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+1
@@ -201,6 +201,7 @@ declare global {
|
|||||||
runProcessOnce: boolean
|
runProcessOnce: boolean
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
GetFileBase64(path: string): Promise<string | null>;
|
GetFileBase64(path: string): Promise<string | null>;
|
||||||
|
TryOpenInstanceInVrc(launchUrl: string): Promise<boolean>;
|
||||||
|
|
||||||
// Folders
|
// Folders
|
||||||
GetVRChatAppDataLocation(): Promise<string>;
|
GetVRChatAppDataLocation(): Promise<string>;
|
||||||
|
|||||||
@@ -1207,7 +1207,8 @@
|
|||||||
@change="
|
@change="
|
||||||
setDiscordWorldIntegration();
|
setDiscordWorldIntegration();
|
||||||
saveDiscordOption();
|
saveDiscordOption();
|
||||||
" />
|
"
|
||||||
|
:tooltip="t('view.settings.discord_presence.discord_presence.world_integration_tooltip')" />
|
||||||
<simple-switch
|
<simple-switch
|
||||||
:label="t('view.settings.discord_presence.discord_presence.instance_type_player_count')"
|
:label="t('view.settings.discord_presence.discord_presence.instance_type_player_count')"
|
||||||
:value="discordInstance"
|
:value="discordInstance"
|
||||||
|
|||||||
Reference in New Issue
Block a user