Last active
April 5, 2025 10:07
-
-
Save Bluscream/7842ad23efb6cbb73f6a1bb17008deed to your computer and use it in GitHub Desktop.
VRCX custom files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | |
playNoty: $app.playNoty, | |
getInstance: API.getInstance, | |
}; | |
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; | |
}); | |
} | |
// 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") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment