console.log("custom.js START") const customjs = { author: "Bluscream", date: "2025-04-02 23:17:32 GMT+1", url: "https://gist.github.com/Bluscream/7842ad23efb6cbb73f6a1bb17008deed" } const steam = { id: "", // TODO: Remove key: "" } // region COMMON let bak = { updateCurrentUserLocation: $app.updateCurrentUserLocation, setCurrentUserLocation: $app.setCurrentUserLocation, applyWorldDialogInstances: $app.applyWorldDialogInstances, applyGroupDialogInstances: $app.applyGroupDialogInstances, eventVrcxMessage: $app.eventVrcxMessage, playNoty: $app.playNoty, getInstance: API.getInstance, SendIpc: AppApi.SendIpc }; const isEmpty = function(v) { return v === null || v === undefined || v === "" } const getTimestamp = function(now = null) { now = now ?? new Date(); const timestamp = now.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); return timestamp; } const formatDateTime = function(now = null) { now = now ?? new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); // const timezone = -now.getTimezoneOffset() / 60; // const timezoneStr = (timezone >= 0 ? '+' : '') + timezone; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} GMT+1`; } const log = function(msg, _alert=true, _notify=false, _noty=false, _noty_type='alert') { console.log(msg); $app.eventVrcxMessage({'MsgType': 'Noty', 'Data': msg }); $app.eventVrcxMessage({'MsgType': 'External', 'UserId': API.currentUser.id, 'Data': msg }); if (_notify) notify("VRCX Addon", msg); // if (_alert) alert(msg); if (_noty) { setTimeout(async () => { await AppApi.DesktopNotification("VRCX Addon", msg) }, 0); // new Noty({type: _noty_type, text: msg}).show(); } // TODO: Fix } const notify = function(title, msg) { async () => { await AppApi.DesktopNotification(title, msg); await AppApi.XSNotification(title, msg, 5000); await AppApi.OVRTNotification(true, true, title, msg, 5000); } } const getLocationObject = async function (loc) { if (typeof loc === 'string') { if (loc.endsWith(')')) loc = $app.parseLocation(loc); else if (loc.startsWith('wrld')) loc = { worldId: loc, world: { id: loc } } else loc = { instanceId: loc, instance: { id: loc } } } else if (isEmpty(loc) || loc === 'traveling:traveling') { return; } if (isEmpty(loc) && !isEmpty($app.lastLocation)) getLocationObject($app.lastLocation); if (isEmpty(loc) && !isEmpty($app.lastLocationDestination)) getLocationObject($app.lastLocationDestination); loc.worldName = await $app.getWorldName(loc); console.log(loc); return loc; } /** * @param {{ notificationId: string }} params * @return { Promise<{json: any, params}> } */ const seeNotification = function(params, emit = true) { return API.call( `auth/user/notifications/${params.notificationId}/see`, { method: 'PUT' } ).then((json) => { const args = { json, params }; if (emit) API.$emit('NOTIFICATION:SEE', args); return args; }); } /** * @param {{ notificationId: string }} params * @return { Promise<{json: any, params}> } */ const hideNotification = function(params, emit = true) { return window.API.call( `auth/user/notifications/${params.notificationId}/hide`, { method: 'PUT' } ).then((json) => { const args = { json, params }; if (emit) window.API.$emit('NOTIFICATION:HIDE', args); return args; }); } const sendInvite = function(params, receiverUserId) { return window.API.call(`invite/${receiverUserId}`, { method: 'POST', params }).then((json) => { const args = { json, params, receiverUserId }; window.API.$emit('NOTIFICATION:INVITE:SEND', args); return args; }); } /** * Updates current user's status. * @param params {SaveCurrentUserParameters} new status to be set * @returns {Promise<{json: any, params}>} */ const saveCurrentUser = function(params) { // https://github.com/vrcx-team/VRCX/blob/4a630079d778069293a39e5b7f7fdb3f543590da/src/api/user.js#L146 return window.API.call(`users/${window.API.currentUser.id}`, { method: 'PUT', params }).then((json) => { var args = { json, params }; window.API.$emit('USER:CURRENT:SAVE', args); return args; }); } const saveBio = function(bio, bioLinks) { // https://github.com/vrcx-team/VRCX/blob/4a630079d778069293a39e5b7f7fdb3f543590da/src/components/dialogs/UserDialog/BioDialog.vue#L74 saveCurrentUser({ bio: bio ?? API.currentUser.bio, bioLinks: bioLinks ?? API.currentUser.bioLinks }) .then((args) => { return args; }); } // endregion COMMON setTimeout(async () => { await AppApi.FocusWindow(); }, 0); // await AppApi.ShowDevTools(); // region AUTO_DISABLE_UNTRUSTED_URLS const REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL = "VRC_ALLOW_UNTRUSTED_URL"; const resetPublicUrls = function() { async () => { const oldVal = await AppApi.GetVRChatRegistryKey(REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL); // if (oldVal) console.log(`${REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL} was ${oldVal}`) await AppApi.SetVRChatRegistryKey(REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL, 0, 3); } } resetPublicUrls(); // let lastRunningState = $app.isGameRunning; setInterval(() => { // if (lastRunningState != $app.isGameRunning && !$app.isGameRunning) { resetPublicUrls(); // } // lastRunningState = $app.isGameRunning; }, 2500); // endregion AUTO_DISABLE_UNTRUSTED_URLS // region IPC_DEBUG // let oldIpcEvent = $app.ipcEvent; // $app.ipcEvent = function (packet) { // console.log(packet); // oldIpcEvent(packet); // } // endregion IPC_DEBUG // region AUTO_INVITE_USER_BUTTON let autoInviteUser, lastInvitedTo, lastJoined; // = null; // bak.setCurrentUserLocation = $app.setCurrentUserLocation; $app.setCurrentUserLocation = function (loc) { // console.log("Before lastLocationDestination ", $app.lastLocationDestination); // console.log("Before lastLocation ", $app.lastLocation); bak.setCurrentUserLocation(); // console.log("After lastLocationDestination ", $app.lastLocationDestination); // console.log("After lastLocation ", $app.lastLocation); // console.log("autoInviteUser ", autoInviteUser); // console.log("lastInvitedTo ", lastInvitedTo); // console.log("lastJoined ", lastJoined); setTimeout(async () => { await onCurrentUserLocationChanged(loc) }, 1000); } // let oldupdateCurrentUserLocation = $app.updateCurrentUserLocation; // $app.updateCurrentUserLocation = function () { // onCurrentUserLocationChanged(API.currentUser.$locationTag) // oldupdateCurrentUserLocation(); // } const onCurrentUserLocationChanged = async function(loc) { console.log(`User Location changed to: ${loc}`) if (loc === 'traveling:traveling') { if (!isEmpty(autoInviteUser) && lastInvitedTo !== loc ) { const userName = `\"${autoInviteUser?.displayName ?? autoInviteUser}\"`; let n; let l = $app.lastLocationDestination; if (isEmpty(l)) { // log(`Cannot invite ${userName}, lastLocationDestination is empty :(`) // return; l = $app.lastLocation.location; n = $app.lastLocation.name; } if (isEmpty(n)) n = await $app.getWorldName(l); // const p = $app.parseLocation(l); log(`Inviting user ${userName} to \"${n}\"`, false) sendInvite({ instanceId: l, worldId: l, worldName: n }, autoInviteUser.id); lastInvitedTo = l; } } else { lastJoined = loc; } } // const onCurrentUserLocationChanged = async function(location) { // console.log(`User Location changed to: ${location}`); // const locationObject = await getLocationObject(location); // if (isEmpty(locationObject)) return; // if (location === 'traveling:traveling') { // if (!isEmpty(autoInviteUser)) { // if (lastInvitedTo === location) return; // log(`Inviting user \"${autoInviteUser?.displayName ?? autoInviteUser}\" to \"${locationObject.worldName}\"`, false); // API.sendInvite(locationObject, autoInviteUser.id); // lastInvitedTo = location; // } // } else { // lastJoined = locationObject; // console.log("lastJoined updated to:", lastJoined); // } // } const toggleAutoInvite = function(user) { if (isEmpty(user) || (!isEmpty(autoInviteUser) && user.id === autoInviteUser?.id)) { log(`Disabled Auto Invite for user ${autoInviteUser.displayName}`); autoInviteUser = null; } else { autoInviteUser = user; log(`Enabled Auto Invite for user ${autoInviteUser.displayName}`); } } const addUserButtons = function(parentNode) { const menuItem = document.createElement('li'); menuItem.className = 'el-dropdown-menu__item el-dropdown-menu__item--divided'; menuItem.tabIndex = '-1'; const icon = document.createElement('i'); icon.className = 'el-icon-message'; menuItem.appendChild(icon); menuItem.onclick = () => { toggleAutoInvite($app.userDialog.ref); }; menuItem.appendChild(document.createTextNode('Auto Invite')); parentNode.appendChild(menuItem); console.log("Added user buttons"); } let buttonsAdded = false; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (!buttonsAdded && mutation.addedNodes.length) { // console.log(`mutations: ${mutation.addedNodes.length}`); mutation.addedNodes.forEach((node) => { // console.log(node.classList); if (node.classList && node.classList.contains('el-dropdown-menu__item')) { console.log("Found user context menu item"); addUserButtons(node.parentElement); buttonsAdded = true; return; } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); // setTimeout(() => { addUserButtons(document.querySelector('.el-dropdown-menu.el-popper.el-dropdown-menu--small')); }, 2500); // endregion AUTO_INVITE_USER_BUTTON /* ` - Friends: {friends} | Blocked: {blocked} | Muted: {muted} VRCX time played: {playtime_total} Steam time played: {playtime_steam} Date joined: {date_joined} Last activity: {last_activity} ago Last updated: {now} (every 2h)` */ // region BIO_TIMER const bioTemplate = ` - Relationship: {partners} <3 Auto Accept: {autojoin} Auto Invite: {autoinvite} Real Rank: {rank} Friends: {friends} | Blocked: {blocked} | Muted: {muted} Time played: {playtime} Date joined: {date_joined} Last updated: {now} (every 2h) User ID: {userId} Steam ID: {steamId} Oculus ID: {oculusId}`; const getSteamPlaytime = async function(steamId, api_key) { try { if (!steamId) { console.log("No Steam ID found"); return null; } const response = await fetch(`https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${api_key}&steamid=${steamId}&appids_filter[0]=438100`); const data = await response.json(); if (!data?.response?.games?.[0]) { console.log("No VRChat playtime data found"); return null; } const playtimeMinutes = data.response.games[0].playtime_forever; console.log(`Got Steam playtime for vrchat: ${playtimeMinutes} minutes`) // const playtimeSeconds = playtimeMinutes * 60; return playtimeMinutes; } catch (error) { console.error("Error getting Steam playtime:", error); return null; } } const changeBio = function(newBio) { try { $app.bioDialog.bio = newBio; $app.saveBio(); } catch (error) { saveBio(newBio); } } const updateBio = async function() { const now = Date.now(); const stats = await database.getUserStats({'id': API.currentUser.id}); const oldBio = API.currentUser.bio.split("\n-\n")[0]; const steamPlayTime = await getSteamPlaytime(steam.id, steam.key); let steamHours, steamSeconds = null; if (steamPlayTime) { steamHours = `${Math.floor(steamPlayTime / 60).toString().padStart(2, '0')}h`; steamSeconds = (steamPlayTime * 60 * 1000); } let playTimeText = $app.timeToText(steamSeconds ?? stats.timeSpent); if (steamHours) playTimeText += ` (${steamHours})`; const moderations = Array.from(API.cachedPlayerModerations.values()); // const last_login = API.currentUser.$last_login; const last_activity = new Date(API.currentUser.last_activity); const favs = Array.from($app.favoriteFriends.values()) const joiners = favs.filter(friend => friend.groupKey === "friend:group_2"); const partners = favs.filter(friend => friend.groupKey === "friend:group_1"); // const onlineText = API.currentUser.$online_for != '' ? `Playing since ${$app.timeToText(now-API.currentUser.$online_for)}`: `Offline since ${$app.timeToText(now-API.currentUser.$offline_for)}`; const newBio = bioTemplate // .replace('{online_offline_since}', onlineText ?? "") .replace('{last_activity}', $app.timeToText(now-last_activity) ?? "") // .replace('{last_login}', $app.timeToText(now-last_login) ?? "") .replace('{playtime}', playTimeText) // .replace('{playtime_total}', $app.timeToText(stats.timeSpent) ?? "Unknown") // .replace('{playtime_steam}', $app.timeToText(steamPlayTime * 60 * 1000) ?? "Unknown") // .replace('{playtime_steam_hours}', steamPlayTime ? steamHours : "Unknown") .replace('{date_joined}', API.currentUser.date_joined ?? "Unknown") .replace('{friends}', API.currentUser.friends.length ?? "?") .replace('{blocked}', moderations.filter(item => item.type === "block").length ?? "?") .replace('{muted}', moderations.filter(item => item.type === "mute").length ?? "?") .replace('{now}', formatDateTime()) .replace('{autojoin}', joiners.map(f => f.name).join(", ")) .replace('{partners}', partners.map(f => f.name).join(", ")) .replace('{autoinvite}', autoInviteUser?.displayName ?? '') .replace('{userId}', API.currentUser.id) .replace('{steamId}', API.currentUser.steamId) .replace('{oculusId}', API.currentUser.oculusId) .replace('{picoId}', API.currentUser.picoId) .replace('{viveId}', API.currentUser.viveId) .replace('{rank}', API.currentUser.$trustLevel) const bio = oldBio + newBio; console.log(`Updating bio to ${bio}`) changeBio(bio); } setInterval(async() => { await updateBio(); }, 7200000); // 2hr setTimeout(async () => { updateBio(); }, 20000); // endregion BIO_TIMER // region OPENVR // (async () => { // await CefSharp.BindObjectAsync('System'); // await CefSharp.BindObjectAsync('VRCXVR'); // await CefSharp.BindObjectAsync('OpenVR'); // OpenVR.System.GetStringTrackedDeviceProperty(0, ETrackedDeviceProperty.Prop_RenderModelName_String, System.Text.StringBuilder(), 5000, null); // //VRCXVR.Instance.StartGameFromPath('C:/Windows/System32/calc.exe', ''); // })(); // endregion OPENVR // region CLIENTUSER_NOTIFICATIONS let lastInvisiblePlayers = 0; let comparePlayerCounts = function() { console.log('Fetching player counts...'); $app.updateCurrentUserLocation(); $app.updateCurrentInstanceWorld(); $app.updateVRLastLocation(); $app.getCurrentInstanceUserList(); setTimeout(() => { console.log('Comparing player counts...'); // const instanceQueue = $app.currentInstanceWorld.instance.queueSize ?? 0; let playerCounts = {}; playerCounts.visiblePlayers = $app.lastLocation?.playerList?.size ?? 0; // blocked players also missing here playerCounts.instanceUserCount = $app.currentInstanceWorld.instance.userCount ?? 0; playerCounts.instanceUsers = $app.currentInstanceWorld.instance.users?.length ?? 0; playerCounts.instancePlatformUsers = ($app.currentInstanceWorld.instance.platforms?.android ?? 0)+($app.currentInstanceWorld.instance.platforms?.ios ?? 0)+($app.currentInstanceWorld.instance.platforms?.standalonewindows ?? 0); playerCounts.n_users = $app.currentInstanceWorld.instance.n_users ?? 0; console.log("Player Count Comparison:"); console.log(`- Visible Players: ${playerCounts.visiblePlayers}`); console.log(`- Instance User Count: ${playerCounts.instanceUserCount}`); console.log(`- Instance Users Length: ${playerCounts.instanceUsers}`); console.log(`- Platform Users Total: ${playerCounts.instancePlatformUsers}`); console.log(`- n_users: ${playerCounts.n_users}`); const invisiblePlayers = playerCounts.n_users - playerCounts.visiblePlayers; console.log(`- lastInvisiblePlayers: ${lastInvisiblePlayers}`); console.log(`- invisiblePlayers: ${invisiblePlayers}`); if (invisiblePlayers > 0) { if (lastInvisiblePlayers === invisiblePlayers) return; const diff = invisiblePlayers - lastInvisiblePlayers; console.log(`- diff: ${diff}`); if (diff === 0) return; // const txt = `${Math.abs(diff)} invisible user${Math.abs(diff) === 1 ? '' : 's'} ${diff > 0 ? 'joined' : 'left'}` const txt = `Invisible user count changed to: ${invisiblePlayers} (${Math.abs(diff)})`; log(txt, false, true); } lastInvisiblePlayers = invisiblePlayers; }, 2000); } // setInterval(() => { comparePlayerCounts(); }, 60000); // 1m // endregion CLIENTUSER_NOTIFICATIONS // region INSTANCE_CLIENTUSER_BADGE API.$on('SHOW_WORLD_DIALOG', (tag) => { console.log(tag); }); // bak.applyWorldDialogInstances = $app.applyWorldDialogInstances; // $app.applyWorldDialogInstances = function (location) { // bak.applyWorldDialogInstances(); // setTimeout(async () => { console.log("applyWorldDialogInstances"); }, 1000); // } // bak.applyGroupDialogInstances = $app.applyGroupDialogInstances; // $app.applyGroupDialogInstances = function (location) { // bak.applyGroupDialogInstances(); // setTimeout(async () => { console.log("applyGroupDialogInstances"); }, 1000); // } // var targetProxy = new Proxy(API.cachedInstances, { // set: function (target, key, value) { // console.log(`${key} set to ${value}`); // target[key] = value; // return true; // } // }); // bak.getInstance = API.getInstance; // API.getInstance = async function(params) { // let instance = bak.getInstance(params); // setTimeout(async () => { console.log("getInstance"); }, 1000); // console.log(instance); // return instance; // } API.getInstance = function (params) { return API.call(`instances/${params.worldId}:${params.instanceId}`, { method: 'GET' }).then((json) => { var args = { json, params }; const users = args.json.userCount; const realUsers = args.json.n_users - args.json.queueSize; args.json.invisiblePlayers = realUsers - users; if (args.json.invisiblePlayers > 0) { // args.json.name = `${args.json.name} (${args.json.invisiblePlayers} invisible)` args.json.displayName = `${args.json.displayName??args.json.name} (${args.json.invisiblePlayers} invisible)` setTimeout(async () => { log(`Found ${args.json.invisiblePlayers} potentially invisible players in instance "${args.json.name}" in world "${args.json.world.name}"`, false, true, true); }, 1000); } API.$emit('INSTANCE', args); return args; }); }; // API.$on('INSTANCE', function (args) { // }); // endregion INSTANCE_CLIENTUSER_BADGE // region BLOCKED_USER_LEAVE // bak.playNoty = $app.methods.playNoty; $app.playNoty = function (json) { setTimeout(() => { bak.playNoty(json); }, 0); let noty = json; let message, image; if (typeof json === 'string') noty, message, image = JSON.parse(json); if (isEmpty(noty)) return; const now = new Date().getTime(); const time = new Date(noty.created_at).getTime(); const diff = now - time; // console.log({ noty: noty, time: time, now: now, diff: diff }); if (diff > 10000) { return; } // noty = $utils.escapeTagRecursive(noty); // message = $utils.escapeTag(message) || ''; switch (noty.type) { case 'BlockedOnPlayerJoined': // console.log(noty); // setTimeout(async () => { await AppApi.QuitGame() }, 0); console.log(noty.type, lastJoined); if (isEmpty(lastJoined)) return; const p = $app.parseLocation(lastJoined); $app.newInstanceSelfInvite(p.worldId); break; case 'invite': console.log(noty); break; } } // endregion BLOCKED_USER_LEAVE // region CUSTOM_TAGS setTimeout(async () => { $app.addCustomTag({'UserId': API.currentUser.id, 'Tag': 'Sexy Mofo', 'TagColour': '#FF00C6'}); $app.addCustomTag({'UserId': 'usr_7e74337e-36c0-48f8-95d5-60e9f7b1d89d', 'Tag': 'Gaylord', 'TagColour': '#FF00C6'}); const msg = `VRCX-Utils started at\n ${getTimestamp()}`; // $app.eventVrcxMessage({'MsgType': 'Noty', 'Data': msg }); log(msg, true, true, true); // $app.eventVrcxMessage({'MsgType': 'External', 'UserId': API.currentUser.id, 'Data': 'External: VRCX-Utils started' }); }, 2500); // endregion CUSTOM_TAGS // region DISMISS_GROUP_NOTIFICATIONS API.$on('NOTIFICATION', function (args) { console.log("NOTIFICATION", args); switch (args.json.type) { case 'group.announcement': // hideNotification(args.params, false); // API. // seeNotification(args.params, false); // // $app.$emit('NOTIFICATION:EXPIRE', args); // // $app.$emit('NOTIFICATION:HIDE', args); // // $app.$emit('NOTIFICATION:SEE', args); // console.log(`Automatically dismissed group notification '${args.json.id}' from "${args.json.data.groupName}"`); break; } }); // API.$on('NOTIFICATION:V2', function (args) { // console.log('NOTIFICATION:V2'); // console.log(args); // }); // endregion DISMISS_GROUP_NOTIFICATIONS // region DUMP_IPC AppApi.SendIpc = function (...args) { console.log("[IPC OUT]", ...args); bak.SendIpc(...args); } $app.eventVrcxMessage = function (...args) { console.log("[IPC IN]", ...args); bak.eventVrcxMessage(...args); } // endregion DUMP_IPC // region CONSOLE_FUNCS const dumpGroups = async function () { const groups = new Set(); for (const friendId of API.currentUser.friends) { try { $app.userDialog.id = friendId; var grps = await $app.getUserGroups(friendId); } catch (error) { if (error.message.includes('429 Too Many Requests')) { console.log('Rate limit reached, stopping group dump'); break; } throw error; } break; } for (const groupId of API.currentUserGroups.keys()) { groups.add(groupId); } for (const groupId of API.cachedGroups.keys()) { groups.add(groupId); } groupsArr = Array.from(groups); groupsArr.sort((a, b) => a.localeCompare(b)); console.log(`Exporting ${groupsArr.length} groups to console`); console.log(Array.prototype.join.call(groupsArr, '\n')); setTimeout(async () => { await AppApi.ShowDevTools(); }, 0); } const searchFunc = function(name_part) { const searchResults = []; const globalVars = Object.keys(window); for (const varName of globalVars) { const variable = window[varName]; if (variable && typeof variable === 'object' && variable !== null) { const properties = Object.getOwnPropertyNames(variable); for (const propName of properties) { const property = variable[propName]; if (typeof property === 'function' && propName.toLowerCase().includes(name_part.toLowerCase())) { searchResults.push({ rootVariable: varName, functionName: propName, function: property }); } } } } return searchResults; } // endregion CONSOLE_FUNCS console.log("custom.js END") const getTimestamp = function(now = null) { now = now ?? new Date(); const timestamp = now.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); return timestamp; } const formatDateTime = function(now = null) { now = now ?? new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); // const timezone = -now.getTimezoneOffset() / 60; // const timezoneStr = (timezone >= 0 ? '+' : '') + timezone; return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} GMT+1`; } const log = function(msg, _alert=true, _notify=false, _noty=false, _noty_type='alert') { console.log(msg); $app.eventVrcxMessage({'MsgType': 'Noty', 'Data': msg }); $app.eventVrcxMessage({'MsgType': 'External', 'UserId': API.currentUser.id, 'Data': msg }); if (_notify) notify("VRCX Addon", msg); // if (_alert) alert(msg); if (_noty) { setTimeout(async () => { await AppApi.DesktopNotification("VRCX Addon", msg) }, 0); // new Noty({type: _noty_type, text: msg}).show(); } // TODO: Fix } const notify = function(title, msg) { async () => { await AppApi.DesktopNotification(title, msg); await AppApi.XSNotification(title, msg, 5000); await AppApi.OVRTNotification(true, true, title, msg, 5000); } } const getLocationObject = async function (loc) { if (typeof loc === 'string') { if (loc.endsWith(')')) loc = $app.parseLocation(loc); else if (loc.startsWith('wrld')) loc = { worldId: loc, world: { id: loc } } else loc = { instanceId: loc, instance: { id: loc } } } else if (isEmpty(loc) || loc === 'traveling:traveling') { return; } if (isEmpty(loc) && !isEmpty($app.lastLocation)) getLocationObject($app.lastLocation); if (isEmpty(loc) && !isEmpty($app.lastLocationDestination)) getLocationObject($app.lastLocationDestination); loc.worldName = await $app.getWorldName(loc); console.log(loc); return loc; } /** * @param {{ notificationId: string }} params * @return { Promise<{json: any, params}> } */ const seeNotification = function(params, emit = true) { return API.call( `auth/user/notifications/${params.notificationId}/see`, { method: 'PUT' } ).then((json) => { const args = { json, params }; if (emit) API.$emit('NOTIFICATION:SEE', args); return args; }); } /** * @param {{ notificationId: string }} params * @return { Promise<{json: any, params}> } */ const hideNotification = function(params, emit = true) { return window.API.call( `auth/user/notifications/${params.notificationId}/hide`, { method: 'PUT' } ).then((json) => { const args = { json, params }; if (emit) window.API.$emit('NOTIFICATION:HIDE', args); return args; }); } // endregion COMMON setTimeout(async () => { await AppApi.FocusWindow(); await AppApi.ShowDevTools(); }, 0); // region AUTO_DISABLE_UNTRUSTED_URLS const REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL = "VRC_ALLOW_UNTRUSTED_URL"; const resetPublicUrls = function() { async () => { const oldVal = await AppApi.GetVRChatRegistryKey(REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL); // if (oldVal) console.log(`${REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL} was ${oldVal}`) await AppApi.SetVRChatRegistryKey(REGISTRY_KEY_VRC_ALLOW_UNTRUSTED_URL, 0, 3); } } resetPublicUrls(); // let lastRunningState = $app.isGameRunning; setInterval(() => { // if (lastRunningState != $app.isGameRunning && !$app.isGameRunning) { resetPublicUrls(); // } // lastRunningState = $app.isGameRunning; }, 2500); // endregion AUTO_DISABLE_UNTRUSTED_URLS // region IPC_DEBUG // let oldIpcEvent = $app.ipcEvent; // $app.ipcEvent = function (packet) { // console.log(packet); // oldIpcEvent(packet); // } // endregion IPC_DEBUG // region AUTO_INVITE_USER_BUTTON let autoInviteUser, lastInvitedTo, lastJoined; // = null; // bak.setCurrentUserLocation = $app.setCurrentUserLocation; $app.setCurrentUserLocation = function (loc) { // console.log("Before lastLocationDestination ", $app.lastLocationDestination); // console.log("Before lastLocation ", $app.lastLocation); bak.setCurrentUserLocation(); // console.log("After lastLocationDestination ", $app.lastLocationDestination); // console.log("After lastLocation ", $app.lastLocation); // console.log("autoInviteUser ", autoInviteUser); // console.log("lastInvitedTo ", lastInvitedTo); // console.log("lastJoined ", lastJoined); setTimeout(async () => { await onCurrentUserLocationChanged(loc) }, 1000); } // let oldupdateCurrentUserLocation = $app.updateCurrentUserLocation; // $app.updateCurrentUserLocation = function () { // onCurrentUserLocationChanged(API.currentUser.$locationTag) // oldupdateCurrentUserLocation(); // } const onCurrentUserLocationChanged = async function(loc) { console.log(`User Location changed to: ${loc}`) if (loc === 'traveling:traveling') { if (!isEmpty(autoInviteUser) && lastInvitedTo !== loc ) { const userName = `\"${autoInviteUser?.displayName ?? autoInviteUser}\"`; let n; let l = $app.lastLocationDestination; if (isEmpty(l)) { // log(`Cannot invite ${userName}, lastLocationDestination is empty :(`) // return; l = $app.lastLocation.location; n = $app.lastLocation.name; } if (isEmpty(n)) n = await $app.getWorldName(l); // const p = $app.parseLocation(l); log(`Inviting user ${userName} to \"${n}\"`, false) API.sendInvite({ instanceId: l, worldId: l, worldName: n }, autoInviteUser.id); lastInvitedTo = l; } } else { lastJoined = loc; } } // const onCurrentUserLocationChanged = async function(location) { // console.log(`User Location changed to: ${location}`); // const locationObject = await getLocationObject(location); // if (isEmpty(locationObject)) return; // if (location === 'traveling:traveling') { // if (!isEmpty(autoInviteUser)) { // if (lastInvitedTo === location) return; // log(`Inviting user \"${autoInviteUser?.displayName ?? autoInviteUser}\" to \"${locationObject.worldName}\"`, false); // API.sendInvite(locationObject, autoInviteUser.id); // lastInvitedTo = location; // } // } else { // lastJoined = locationObject; // console.log("lastJoined updated to:", lastJoined); // } // } const toggleAutoInvite = function(user) { if (isEmpty(user) || (!isEmpty(autoInviteUser) && user.id === autoInviteUser?.id)) { log(`Disabled Auto Invite for user ${autoInviteUser.displayName}`); autoInviteUser = null; } else { autoInviteUser = user; log(`Enabled Auto Invite for user ${autoInviteUser.displayName}`); } } const addUserButtons = function(parentNode) { const menuItem = document.createElement('li'); menuItem.className = 'el-dropdown-menu__item el-dropdown-menu__item--divided'; menuItem.tabIndex = '-1'; const icon = document.createElement('i'); icon.className = 'el-icon-message'; menuItem.appendChild(icon); menuItem.onclick = () => { toggleAutoInvite($app.userDialog.ref); }; menuItem.appendChild(document.createTextNode('Auto Invite')); parentNode.appendChild(menuItem); console.log("Added user buttons"); } let buttonsAdded = false; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (!buttonsAdded && mutation.addedNodes.length) { // console.log(`mutations: ${mutation.addedNodes.length}`); mutation.addedNodes.forEach((node) => { // console.log(node.classList); if (node.classList && node.classList.contains('el-dropdown-menu__item')) { console.log("Found user context menu item"); addUserButtons(node.parentElement); buttonsAdded = true; return; } }); } }); }); observer.observe(document.body, { childList: true, subtree: true }); // setTimeout(() => { addUserButtons(document.querySelector('.el-dropdown-menu.el-popper.el-dropdown-menu--small')); }, 2500); // endregion AUTO_INVITE_USER_BUTTON /* ` - Friends: {friends} | Blocked: {blocked} | Muted: {muted} VRCX time played: {playtime_total} Steam time played: {playtime_steam} Date joined: {date_joined} Last activity: {last_activity} ago Last updated: {now} (every 2h)` */ // region BIO_TIMER const bioTemplate = ` - Relationship: {partners} <3 Auto Accept: {autojoin} Auto Invite: {autoinvite} Real Rank: {rank} Friends: {friends} | Blocked: {blocked} | Muted: {muted} Time played: {playtime_steam} ({playtime_steam_hours}) Date joined: {date_joined} Last updated: {now} (every 2h) User ID: {userId} Steam ID: {steamId} Oculus ID: {oculusId}`; const getSteamPlaytime = async function(steamId, api_key) { try { if (!steamId) { console.log("No Steam ID found"); return null; } const response = await fetch(`https://api.steampowered.com/IPlayerService/GetOwnedGames/v1/?key=${api_key}&steamid=${steamId}&appids_filter[0]=438100`); const data = await response.json(); if (!data?.response?.games?.[0]) { console.log("No VRChat playtime data found"); return null; } const playtimeMinutes = data.response.games[0].playtime_forever; console.log(`Got Steam playtime for vrchat: ${playtimeMinutes} minutes`) // const playtimeSeconds = playtimeMinutes * 60; return playtimeMinutes; } catch (error) { console.error("Error getting Steam playtime:", error); return null; } } const changeBio = function(newBio) { $app.bioDialog.bio = newBio; $app.saveBio(); } const updateBio = async function() { const now = Date.now(); const stats = await database.getUserStats({'id': API.currentUser.id}); const oldBio = API.currentUser.bio.split("\n-\n")[0]; const steamPlayTime = await getSteamPlaytime(steam.id, steam.key); const steamHours = `${Math.floor(steamPlayTime / 60).toString().padStart(2, '0')}h`; const moderations = Array.from(API.cachedPlayerModerations.values()); // const last_login = API.currentUser.$last_login; const last_activity = new Date(API.currentUser.last_activity); const favs = Array.from($app.favoriteFriends.values()) const joiners = favs.filter(friend => friend.groupKey === "friend:group_2"); const partners = favs.filter(friend => friend.groupKey === "friend:group_1"); // const onlineText = API.currentUser.$online_for != '' ? `Playing since ${$app.timeToText(now-API.currentUser.$online_for)}`: `Offline since ${$app.timeToText(now-API.currentUser.$offline_for)}`; const newBio = bioTemplate // .replace('{online_offline_since}', onlineText ?? "") .replace('{last_activity}', $app.timeToText(now-last_activity) ?? "") // .replace('{last_login}', $app.timeToText(now-last_login) ?? "") .replace('{playtime_total}', $app.timeToText(stats.timeSpent) ?? "Unknown") .replace('{playtime_steam}', $app.timeToText(steamPlayTime * 60 * 1000) ?? "Unknown") .replace('{playtime_steam_hours}', steamHours ?? "Unknown") .replace('{date_joined}', API.currentUser.date_joined ?? "Unknown") .replace('{friends}', API.currentUser.friends.length ?? "?") .replace('{blocked}', moderations.filter(item => item.type === "block").length ?? "?") .replace('{muted}', moderations.filter(item => item.type === "mute").length ?? "?") .replace('{now}', formatDateTime()) .replace('{autojoin}', joiners.map(f => f.name).join(", ")) .replace('{partners}', partners.map(f => f.name).join(", ")) .replace('{autoinvite}', autoInviteUser?.displayName ?? '') .replace('{userId}', API.currentUser.id) .replace('{steamId}', API.currentUser.steamId) .replace('{oculusId}', API.currentUser.oculusId) .replace('{picoId}', API.currentUser.picoId) .replace('{viveId}', API.currentUser.viveId) .replace('{rank}', API.currentUser.$trustLevel) const bio = oldBio + newBio; console.log(`Updating bio to ${bio}`) changeBio(bio); } setInterval(async() => { await updateBio(); }, 7200000); // 2hr setTimeout(async () => { updateBio(); }, 20000); // endregion BIO_TIMER // region OPENVR // (async () => { // await CefSharp.BindObjectAsync('System'); // await CefSharp.BindObjectAsync('VRCXVR'); // await CefSharp.BindObjectAsync('OpenVR'); // OpenVR.System.GetStringTrackedDeviceProperty(0, ETrackedDeviceProperty.Prop_RenderModelName_String, System.Text.StringBuilder(), 5000, null); // //VRCXVR.Instance.StartGameFromPath('C:/Windows/System32/calc.exe', ''); // })(); // endregion OPENVR // region CLIENTUSER_NOTIFICATIONS let lastInvisiblePlayers = 0; let comparePlayerCounts = function() { console.log('Fetching player counts...'); $app.updateCurrentUserLocation(); $app.updateCurrentInstanceWorld(); $app.updateVRLastLocation(); $app.getCurrentInstanceUserList(); setTimeout(() => { console.log('Comparing player counts...'); // const instanceQueue = $app.currentInstanceWorld.instance.queueSize ?? 0; let playerCounts = {}; playerCounts.visiblePlayers = $app.lastLocation?.playerList?.size ?? 0; // blocked players also missing here playerCounts.instanceUserCount = $app.currentInstanceWorld.instance.userCount ?? 0; playerCounts.instanceUsers = $app.currentInstanceWorld.instance.users?.length ?? 0; playerCounts.instancePlatformUsers = ($app.currentInstanceWorld.instance.platforms?.android ?? 0)+($app.currentInstanceWorld.instance.platforms?.ios ?? 0)+($app.currentInstanceWorld.instance.platforms?.standalonewindows ?? 0); playerCounts.n_users = $app.currentInstanceWorld.instance.n_users ?? 0; console.log("Player Count Comparison:"); console.log(`- Visible Players: ${playerCounts.visiblePlayers}`); console.log(`- Instance User Count: ${playerCounts.instanceUserCount}`); console.log(`- Instance Users Length: ${playerCounts.instanceUsers}`); console.log(`- Platform Users Total: ${playerCounts.instancePlatformUsers}`); console.log(`- n_users: ${playerCounts.n_users}`); const invisiblePlayers = playerCounts.n_users - playerCounts.visiblePlayers; console.log(`- lastInvisiblePlayers: ${lastInvisiblePlayers}`); console.log(`- invisiblePlayers: ${invisiblePlayers}`); if (invisiblePlayers > 0) { if (lastInvisiblePlayers === invisiblePlayers) return; const diff = invisiblePlayers - lastInvisiblePlayers; console.log(`- diff: ${diff}`); if (diff === 0) return; // const txt = `${Math.abs(diff)} invisible user${Math.abs(diff) === 1 ? '' : 's'} ${diff > 0 ? 'joined' : 'left'}` const txt = `Invisible user count changed to: ${invisiblePlayers} (${Math.abs(diff)})`; log(txt, false, true); } lastInvisiblePlayers = invisiblePlayers; }, 2000); } // setInterval(() => { comparePlayerCounts(); }, 60000); // 1m // endregion CLIENTUSER_NOTIFICATIONS // region INSTANCE_CLIENTUSER_BADGE API.$on('SHOW_WORLD_DIALOG', (tag) => { console.log(tag); }); // bak.applyWorldDialogInstances = $app.applyWorldDialogInstances; // $app.applyWorldDialogInstances = function (location) { // bak.applyWorldDialogInstances(); // setTimeout(async () => { console.log("applyWorldDialogInstances"); }, 1000); // } // bak.applyGroupDialogInstances = $app.applyGroupDialogInstances; // $app.applyGroupDialogInstances = function (location) { // bak.applyGroupDialogInstances(); // setTimeout(async () => { console.log("applyGroupDialogInstances"); }, 1000); // } // var targetProxy = new Proxy(API.cachedInstances, { // set: function (target, key, value) { // console.log(`${key} set to ${value}`); // target[key] = value; // return true; // } // }); // bak.getInstance = API.getInstance; // API.getInstance = async function(params) { // let instance = bak.getInstance(params); // setTimeout(async () => { console.log("getInstance"); }, 1000); // console.log(instance); // return instance; // } API.getInstance = function (params) { return API.call(`instances/${params.worldId}:${params.instanceId}`, { method: 'GET' }).then((json) => { var args = { json, params }; const users = args.json.userCount; const realUsers = args.json.n_users - args.json.queueSize; args.json.invisiblePlayers = realUsers - users; if (args.json.invisiblePlayers > 0) { // args.json.name = `${args.json.name} (${args.json.invisiblePlayers} invisible)` args.json.displayName = `${args.json.displayName??args.json.name} (${args.json.invisiblePlayers} invisible)` setTimeout(async () => { log(`Found ${args.json.invisiblePlayers} potentially invisible players in instance "${args.json.name}" in world "${args.json.world.name}"`, false, true, true); }, 1000); } API.$emit('INSTANCE', args); return args; }); }; // API.$on('INSTANCE', function (args) { // }); // endregion INSTANCE_CLIENTUSER_BADGE // region BLOCKED_USER_LEAVE // bak.playNoty = $app.methods.playNoty; $app.playNoty = function (json) { setTimeout(() => { bak.playNoty(json); }, 0); let noty = json; let message, image; if (typeof json === 'string') noty, message, image = JSON.parse(json); if (isEmpty(noty)) return; const now = new Date().getTime(); const time = new Date(noty.created_at).getTime(); const diff = now - time; // console.log({ noty: noty, time: time, now: now, diff: diff }); if (diff > 10000) { return; } // noty = $utils.escapeTagRecursive(noty); // message = $utils.escapeTag(message) || ''; switch (noty.type) { case 'BlockedOnPlayerJoined': // console.log(noty); // setTimeout(async () => { await AppApi.QuitGame() }, 0); console.log(noty.type, lastJoined); if (isEmpty(lastJoined)) return; const p = $app.parseLocation(lastJoined); $app.newInstanceSelfInvite(p.worldId); break; case 'invite': console.log(noty); break; } } // endregion BLOCKED_USER_LEAVE // region CUSTOM_TAGS setTimeout(async () => { $app.addCustomTag({'UserId': API.currentUser.id, 'Tag': 'Sexy Mofo', 'TagColour': '#FF00C6'}); $app.addCustomTag({'UserId': 'usr_7e74337e-36c0-48f8-95d5-60e9f7b1d89d', 'Tag': 'Gaylord', 'TagColour': '#FF00C6'}); const msg = `VRCX-Utils started at\n ${getTimestamp()}`; // $app.eventVrcxMessage({'MsgType': 'Noty', 'Data': msg }); log(msg, true, true, true); // $app.eventVrcxMessage({'MsgType': 'External', 'UserId': API.currentUser.id, 'Data': 'External: VRCX-Utils started' }); }, 2500); // endregion CUSTOM_TAGS // region DISMISS_GROUP_NOTIFICATIONS API.$on('NOTIFICATION', function (args) { console.log("NOTIFICATION", args); switch (args.json.type) { case 'group.announcement': hideNotification(args.params, false); // API. seeNotification(args.params, false); // $app.$emit('NOTIFICATION:EXPIRE', args); // $app.$emit('NOTIFICATION:HIDE', args); // $app.$emit('NOTIFICATION:SEE', args); console.log(`Automatically dismissed group notification '${args.json.id}' from "${args.json.data.groupName}"`); break; } }); // endregion DISMISS_GROUP_NOTIFICATIONS // API.$on('NOTIFICATION:V2', function (args) { // console.log('NOTIFICATION:V2'); // console.log(args); // }); console.log("custom.js END")