import { computed, reactive, ref } from 'vue'; import { ElMessage, ElMessageBox } from 'element-plus'; import { defineStore } from 'pinia'; import { useI18n } from 'vue-i18n'; import { checkVRChatCache, displayLocation, getGroupName, getWorldName, parseLocation, removeFromArray, replaceBioSymbols, timeToText } from '../shared/utils'; import { instanceRequest, userRequest } from '../api'; import { photonEmojis, photonEventType } from '../shared/constants/photon'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useAvatarStore } from './avatar'; import { useFavoriteStore } from './favorite'; import { useFriendStore } from './friend'; import { useGameLogStore } from './gameLog'; import { useInstanceStore } from './instance'; import { useLocationStore } from './location'; import { useNotificationStore } from './notification'; import { useSharedFeedStore } from './sharedFeed'; import { useUserStore } from './user'; import { useVrStore } from './vr'; import configRepository from '../service/config'; import * as workerTimers from 'worker-timers'; export const usePhotonStore = defineStore('Photon', () => { const vrStore = useVrStore(); const userStore = useUserStore(); const friendStore = useFriendStore(); const instanceStore = useInstanceStore(); const gameLogStore = useGameLogStore(); const notificationStore = useNotificationStore(); const locationStore = useLocationStore(); const sharedFeedStore = useSharedFeedStore(); const avatarStore = useAvatarStore(); const favoriteStore = useFavoriteStore(); const { t } = useI18n(); const state = reactive({ photonEventTableTypeFilterList: [ 'Event', 'OnPlayerJoined', 'OnPlayerLeft', 'ChangeAvatar', 'ChangeStatus', 'ChangeGroup', 'PortalSpawn', 'DeletedPortal', 'ChatBoxMessage', 'Moderation', 'Camera', 'SpawnEmoji', 'MasterMigrate' ], photonEventCount: 0, photonLobbyTimeoutThreshold: 6000, photonOverlayMessageTimeout: 6000, // photonLobbyWatcherLoop: false }); const photonLobby = ref(new Map()); const photonLobbyMaster = ref(0); const photonLobbyCurrentUser = ref(0); const photonLobbyUserData = ref(new Map()); const photonLobbyCurrent = ref(new Map()); const photonLobbyAvatars = ref(new Map()); const photonLobbyLastModeration = ref(new Map()); const photonLobbyTimeout = ref([]); const photonLobbyJointime = ref(new Map()); const photonLobbyActivePortals = ref(new Map()); const photonEvent7List = ref(new Map()); const photonLastEvent7List = ref(0); const photonLastChatBoxMsg = ref(new Map()); const moderationEventQueue = ref(new Map()); const photonLoggingEnabled = ref(false); const photonEventOverlay = ref(false); const photonEventOverlayFilter = ref('Everyone'); const photonEventTableTypeOverlayFilter = ref([]); const timeoutHudOverlay = ref(false); const timeoutHudOverlayFilter = ref('Everyone'); const photonEventIcon = ref(false); const photonEventTable = ref({ data: [], filters: [ { prop: ['displayName', 'text'], value: '' }, { prop: 'type', value: [] } ], tableProps: { stripe: true, size: 'small' }, pageSize: 10, paginationProps: { layout: 'sizes,prev,pager,next,total' } }); const photonEventTablePrevious = ref({ data: [], filters: [ { prop: ['displayName', 'text'], value: '' }, { prop: 'type', value: [] } ], tableProps: { stripe: true, size: 'small' }, pageSize: 10, paginationProps: { layout: 'sizes,prev,pager,next,total' } }); const chatboxUserBlacklist = ref(new Map()); const chatboxBlacklist = ref([ 'NP: ', 'Now Playing', 'Now playing', "▶️ '", '( ▶️ ', "' - '", "' by '", '[Spotify] ' ]); const photonEventTableFilter = ref(''); const moderationAgainstTable = ref([]); const photonEventTableTypeFilter = ref([]); async function initPhotonStates() { const [ photonEventOverlayConfig, photonEventOverlayFilterConfig, photonEventTableTypeOverlayFilterConfig, timeoutHudOverlayConfig, timeoutHudOverlayFilterConfig, photonLobbyTimeoutThresholdConfig, photonOverlayMessageTimeoutConfig, photonEventTableTypeFilterConfig, chatboxUserBlacklistConfig, chatboxKeywordBlacklistConfig ] = await Promise.all([ configRepository.getBool('VRCX_PhotonEventOverlay', false), configRepository.getString( 'VRCX_PhotonEventOverlayFilter', 'Everyone' ), configRepository.getString( 'VRCX_photonEventTypeOverlayFilter', '[]' ), configRepository.getBool('VRCX_TimeoutHudOverlay', false), configRepository.getString( 'VRCX_TimeoutHudOverlayFilter', 'Everyone' ), configRepository.getInt('VRCX_photonLobbyTimeoutThreshold', 6000), configRepository.getString( 'VRCX_photonOverlayMessageTimeout', (6000).toString() ), configRepository.getString('VRCX_photonEventTypeFilter', '[]'), configRepository.getString('VRCX_chatboxUserBlacklist'), configRepository.getString('VRCX_chatboxBlacklist') ]); photonEventOverlay.value = photonEventOverlayConfig; photonEventOverlayFilter.value = photonEventOverlayFilterConfig; photonEventTableTypeOverlayFilter.value = JSON.parse( photonEventTableTypeOverlayFilterConfig ); timeoutHudOverlay.value = timeoutHudOverlayConfig; timeoutHudOverlayFilter.value = timeoutHudOverlayFilterConfig; photonLobbyTimeoutThreshold.value = photonLobbyTimeoutThresholdConfig; photonOverlayMessageTimeout.value = Number( photonOverlayMessageTimeoutConfig ); photonEventTableTypeFilter.value = JSON.parse( photonEventTableTypeFilterConfig ); photonEventTable.value.filters[1].value = photonEventTableTypeFilter.value; photonEventTablePrevious.value.filters[1].value = photonEventTableTypeFilter.value; chatboxUserBlacklist.value = new Map( Object.entries(JSON.parse(chatboxUserBlacklistConfig || '{}')) ); try { if (chatboxKeywordBlacklistConfig) { const arr = JSON.parse(chatboxKeywordBlacklistConfig); if (Array.isArray(arr)) { chatboxBlacklist.value = arr; } } } catch (err) { console.error( 'Failed to parse chatbox keyword blacklist config', err ); } } initPhotonStates(); const photonLobbyTimeoutThreshold = computed({ get: () => state.photonLobbyTimeoutThreshold, set: (value) => { state.photonLobbyTimeoutThreshold = value; configRepository.setString( 'VRCX_photonLobbyTimeoutThreshold', value.toString() ); } }); const photonOverlayMessageTimeout = computed({ get: () => state.photonOverlayMessageTimeout, set: (value) => { state.photonOverlayMessageTimeout = value; configRepository.setString( 'VRCX_photonOverlayMessageTimeout', value.toString() ); } }); function setPhotonLoggingEnabled() { photonLoggingEnabled.value = !photonLoggingEnabled.value; configRepository.setBool('VRCX_photonLoggingEnabled', true); } function setPhotonEventOverlay() { photonEventOverlay.value = !photonEventOverlay.value; configRepository.setBool( 'VRCX_PhotonEventOverlay', photonEventOverlay.value ); } function setPhotonEventOverlayFilter(value) { photonEventOverlayFilter.value = value; configRepository.setString( 'VRCX_PhotonEventOverlayFilter', photonEventOverlayFilter.value ); } function setPhotonEventTableTypeOverlayFilter(value) { photonEventTableTypeOverlayFilter.value = value; configRepository.setString( 'VRCX_photonEventTypeOverlayFilter', JSON.stringify(photonEventTableTypeOverlayFilter.value) ); } function setTimeoutHudOverlay(value) { timeoutHudOverlay.value = !timeoutHudOverlay.value; configRepository.setBool('VRCX_TimeoutHudOverlay', value); if (!timeoutHudOverlay.value) { AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); } } function setTimeoutHudOverlayFilter(value) { timeoutHudOverlayFilter.value = value; configRepository.setString( 'VRCX_TimeoutHudOverlayFilter', timeoutHudOverlayFilter.value ); } function getDisplayName(userId) { if (userId) { const ref = userStore.cachedUsers.get(userId); if (ref.displayName) { return ref.displayName; } } return ''; } function photonEventPulse() { state.photonEventCount++; photonEventIcon.value = true; workerTimers.setTimeout(() => (photonEventIcon.value = false), 150); } async function saveEventOverlay(configKey = '') { if (configKey === 'VRCX_PhotonEventOverlay') { setPhotonEventOverlay(); } else if (configKey === 'VRCX_TimeoutHudOverlay') { setTimeoutHudOverlay(); } vrStore.updateOpenVR(); vrStore.updateVRConfigVars(); } function parseOperationResponse(data, dateTime) { switch (data.OperationCode) { case 226: if ( typeof data.Parameters[248] !== 'undefined' && typeof data.Parameters[248][248] !== 'undefined' ) { setPhotonLobbyMaster(data.Parameters[248][248]); } if (typeof data.Parameters[254] !== 'undefined') { photonLobbyCurrentUser.value = data.Parameters[254]; } if (typeof data.Parameters[249] !== 'undefined') { for (const i in data.Parameters[249]) { const idNum = safeParseInt(i); if (idNum === null) continue; const user = data.Parameters[249][i]; if (!user || !user.user) continue; parsePhotonUser(idNum, user.user, dateTime); if (user.avatarDict) { parsePhotonAvatarChange( idNum, user.user, user.avatarDict, dateTime ); parsePhotonAvatar(user.avatarDict); } if (user.favatarDict) { parsePhotonAvatar(user.favatarDict); } if (typeof user.groupOnNameplate !== 'undefined') { parsePhotonGroupChange( idNum, user.user, user.groupOnNameplate, dateTime ); } let hasInstantiated = false; const lobbyJointime = photonLobbyJointime.value.get(idNum); if (typeof lobbyJointime !== 'undefined') { hasInstantiated = lobbyJointime.hasInstantiated; } photonLobbyJointime.value.set(idNum, { joinTime: Date.parse(dateTime), hasInstantiated, inVRMode: user.inVRMode, avatarEyeHeight: user.avatarEyeHeight, canModerateInstance: user.canModerateInstance, groupOnNameplate: user.groupOnNameplate, showGroupBadgeToOthers: user.showGroupBadgeToOthers, showSocialRank: user.showSocialRank, useImpostorAsFallback: user.useImpostorAsFallback, platform: user.platform }); } } if (typeof data.Parameters[252] !== 'undefined') { parsePhotonLobbyIds(data.Parameters[252]); } photonEvent7List.value = new Map(); break; } } function checkChatboxBlacklist(msg) { if (typeof msg !== 'string') return false; const list = chatboxBlacklist.value || []; for (let i = 0; i < list.length; ++i) { const kw = list[i]; if (!kw) continue; if (msg.includes(kw)) return true; } return false; } async function saveChatboxUserBlacklist() { await configRepository.setString( 'VRCX_chatboxUserBlacklist', JSON.stringify(Object.fromEntries(chatboxUserBlacklist.value)) ); } async function saveChatboxBlacklist() { await configRepository.setString( 'VRCX_chatboxBlacklist', JSON.stringify(chatboxBlacklist.value) ); } async function photonEventTableFilterChange() { photonEventTable.value.filters[0].value = photonEventTableFilter.value; photonEventTable.value.filters[1].value = photonEventTableTypeFilter.value; photonEventTablePrevious.value.filters[0].value = photonEventTableFilter.value; photonEventTablePrevious.value.filters[1].value = photonEventTableTypeFilter.value; await configRepository.setString( 'VRCX_photonEventTypeFilter', JSON.stringify(photonEventTableTypeFilter.value) ); // await configRepository.setString( // 'VRCX_photonEventTypeOverlayFilter', // JSON.stringify(this.photonEventTableTypeOverlayFilter) // ); } function showUserFromPhotonId(photonId) { if (photonId) { const ref = photonLobby.value.get(photonId); if (typeof ref !== 'undefined') { if (typeof ref.id !== 'undefined') { userStore.showUserDialog(ref.id); } else if (typeof ref.displayName !== 'undefined') { userStore.lookupUser(ref); } } else { ElMessage({ message: 'No user info available', type: 'error' }); } } } function promptPhotonOverlayMessageTimeout() { ElMessageBox.prompt( t('prompt.overlay_message_timeout.description'), t('prompt.overlay_message_timeout.header'), { distinguishCancelAndClose: true, confirmButtonText: t('prompt.overlay_message_timeout.ok'), cancelButtonText: t('prompt.overlay_message_timeout.cancel'), inputValue: ( state.photonOverlayMessageTimeout / 1000 ).toString(), inputPattern: /\d+$/, inputErrorMessage: t( 'prompt.overlay_message_timeout.input_error' ) } ) .then(({ value, action }) => { if (action === 'confirm' && value && !isNaN(Number(value))) { state.photonOverlayMessageTimeout = Math.trunc( Number(value) * 1000 ); vrStore.updateVRConfigVars(); } }) .catch(() => {}); } function promptPhotonLobbyTimeoutThreshold() { ElMessageBox.prompt( t('prompt.photon_lobby_timeout.description'), t('prompt.photon_lobby_timeout.header'), { distinguishCancelAndClose: true, confirmButtonText: t('prompt.photon_lobby_timeout.ok'), cancelButtonText: t('prompt.photon_lobby_timeout.cancel'), inputValue: ( state.photonLobbyTimeoutThreshold / 1000 ).toString(), inputPattern: /\d+$/, inputErrorMessage: t('prompt.photon_lobby_timeout.input_error') } ) .then(({ value, action }) => { if (action === 'confirm' && value && !isNaN(Number(value))) { state.photonLobbyTimeoutThreshold = Math.trunc( Number(value) * 1000 ); } }) .catch(() => {}); } function startLobbyWatcherLoop() { if (!state.photonLobbyWatcherLoop) { state.photonLobbyWatcherLoop = true; photonLobbyWatcher(); } } function photonLobbyWatcherLoopStop() { state.photonLobbyWatcherLoop = false; photonLobbyTimeout.value = []; AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); } function photonLobbyWatcher() { if (!state.photonLobbyWatcherLoop) { return; } if (photonLobbyCurrent.value.size === 0) { photonLobbyWatcherLoopStop(); return; } const dtNow = Date.now(); const bias2 = photonLastEvent7List.value + 1.5 * 1000; if (dtNow > bias2 || locationStore.lastLocation.playerList.size <= 1) { if (photonLobbyTimeout.value.length > 0) { AppApi.ExecuteVrOverlayFunction('updateHudTimeout', '[]'); } photonLobbyTimeout.value = []; workerTimers.setTimeout(() => photonLobbyWatcher(), 500); return; } const hudTimeout = []; photonEvent7List.value.forEach((dt, id) => { const timeSinceLastEvent = dtNow - Date.parse(dt); if ( timeSinceLastEvent > state.photonLobbyTimeoutThreshold && id !== photonLobbyCurrentUser.value ) { if (photonLobbyJointime.value.has(id)) { var { joinTime } = photonLobbyJointime.value.get(id); } if (!joinTime) { console.log(`${id} missing join time`); } if (joinTime && joinTime + 70000 < dtNow) { // wait 70secs for user to load in hudTimeout.unshift({ userId: getUserIdFromPhotonId(id), displayName: getDisplayNameFromPhotonId(id), time: Math.round(timeSinceLastEvent / 1000), rawTime: timeSinceLastEvent }); } } }); if (photonLobbyTimeout.value.length > 0 || hudTimeout.length > 0) { hudTimeout.sort(function (a, b) { if (a.rawTime > b.rawTime) { return 1; } if (a.rawTime < b.rawTime) { return -1; } return 0; }); if (timeoutHudOverlay.value) { if ( timeoutHudOverlayFilter.value === 'VIP' || timeoutHudOverlayFilter.value === 'Friends' ) { var filteredHudTimeout = []; hudTimeout.forEach((item) => { if ( timeoutHudOverlayFilter.value === 'VIP' && favoriteStore.getCachedFavoritesByObjectId( item.userId ) ) { filteredHudTimeout.push(item); } else if ( timeoutHudOverlayFilter.value === 'Friends' && friendStore.friends.has(item.userId) ) { filteredHudTimeout.push(item); } }); } else { var filteredHudTimeout = hudTimeout; } AppApi.ExecuteVrOverlayFunction( 'updateHudTimeout', JSON.stringify(filteredHudTimeout) ); } photonLobbyTimeout.value = hudTimeout; instanceStore.getCurrentInstanceUserList(); } workerTimers.setTimeout(() => photonLobbyWatcher(), 500); } function addEntryPhotonEvent(input) { let isMaster = false; if (input.photonId === photonLobbyMaster.value) { isMaster = true; } const joinTimeRef = photonLobbyJointime.value.get(input.photonId); const isModerator = joinTimeRef?.canModerateInstance; const photonUserRef = photonLobby.value.get(input.photonId); let displayName = ''; let userId = ''; let isFriend = false; if (typeof photonUserRef !== 'undefined') { displayName = photonUserRef.displayName; userId = photonUserRef.id; isFriend = photonUserRef.isFriend; } const isFavorite = friendStore.localFavoriteFriends.has(userId); let colour = ''; const tagRef = userStore.customUserTags.get(userId); if (typeof tagRef !== 'undefined') { colour = tagRef.colour; } const feed = { displayName, userId, isFavorite, isFriend, isMaster, isModerator, colour, ...input }; photonEventTable.value.data.unshift(feed); if ( photonEventTableTypeOverlayFilter.value.length > 0 && !photonEventTableTypeOverlayFilter.value.includes(feed.type) ) { return; } if (photonEventOverlay.value) { if ( photonEventOverlayFilter.value === 'VIP' || photonEventOverlayFilter.value === 'Friends' ) { if ( feed.userId && ((photonEventOverlayFilter.value === 'VIP' && isFavorite) || (photonEventOverlayFilter.value === 'Friends' && isFriend)) ) { AppApi.ExecuteVrOverlayFunction( 'addEntryHudFeed', JSON.stringify(feed) ); } } else { AppApi.ExecuteVrOverlayFunction( 'addEntryHudFeed', JSON.stringify(feed) ); } } } function getDisplayNameFromPhotonId(photonId) { let displayName = ''; if (photonId) { const ref = photonLobby.value.get(photonId); displayName = `ID:${photonId}`; if ( typeof ref !== 'undefined' && typeof ref.displayName !== 'undefined' ) { displayName = ref.displayName; } } return displayName; } function getUserIdFromPhotonId(photonId) { let userId = ''; if (photonId) { const ref = photonLobby.value.get(photonId); if (typeof ref !== 'undefined' && typeof ref.id !== 'undefined') { userId = ref.id; } } return userId; } function getPhotonIdFromDisplayName(displayName) { let photonId = ''; if (displayName) { photonLobby.value.forEach((ref, id) => { if ( typeof ref !== 'undefined' && ref.displayName === displayName ) { photonId = id; } }); } return photonId; } function getPhotonIdFromUserId(userId) { let photonId = ''; if (userId) { photonLobby.value.forEach((ref, id) => { if (typeof ref !== 'undefined' && ref.id === userId) { photonId = id; } }); } return photonId; } // function sortPhotonId(a, b, field) { // const id1 = getPhotonIdFromDisplayName(a[field]); // const id2 = getPhotonIdFromDisplayName(b[field]); // if (id1 < id2) { // return 1; // } // if (id1 > id2) { // return -1; // } // return 0; // } function parsePhotonEvent(data, gameLogDate) { switch (data.Code) { case 253: // SetUserProperties if (data.Parameters[253] === -1) { for (let i in data.Parameters[251]) { const idNum = safeParseInt(i); if (idNum === null) continue; const user = data.Parameters[251][i]; if (!user || !user.user) continue; parsePhotonUser(idNum, user.user, gameLogDate); if (user.avatarDict) { parsePhotonAvatarChange( idNum, user.user, user.avatarDict, gameLogDate ); parsePhotonAvatar(user.avatarDict); } if (user.favatarDict) { parsePhotonAvatar(user.favatarDict); } if (typeof user.groupOnNameplate !== 'undefined') { parsePhotonGroupChange( idNum, user.user, user.groupOnNameplate, gameLogDate ); } var hasInstantiated = false; var lobbyJointime = photonLobbyJointime.value.get(idNum); if (typeof lobbyJointime !== 'undefined') { hasInstantiated = lobbyJointime.hasInstantiated; } photonLobbyJointime.value.set(idNum, { joinTime: Date.parse(gameLogDate), hasInstantiated, inVRMode: user.inVRMode, avatarEyeHeight: user.avatarEyeHeight, canModerateInstance: user.canModerateInstance, groupOnNameplate: user.groupOnNameplate, showGroupBadgeToOthers: user.showGroupBadgeToOthers, showSocialRank: user.showSocialRank, useImpostorAsFallback: user.useImpostorAsFallback, platform: user.platform }); photonUserJoin(idNum, user, gameLogDate); } } else { console.log('oldSetUserProps', data); const idNum = safeParseInt(data.Parameters[253]); if (idNum === null) break; const user = data.Parameters[251]; if (!user || !user.user) break; parsePhotonUser(idNum, user.user, gameLogDate); if (user.avatarDict) { parsePhotonAvatarChange( idNum, user.user, user.avatarDict, gameLogDate ); parsePhotonAvatar(user.avatarDict); } if (user.favatarDict) { parsePhotonAvatar(user.favatarDict); } if (typeof user.groupOnNameplate !== 'undefined') { parsePhotonGroupChange( idNum, user.user, user.groupOnNameplate, gameLogDate ); } var hasInstantiated = false; var lobbyJointime = photonLobbyJointime.value.get(idNum); if (typeof lobbyJointime !== 'undefined') { hasInstantiated = lobbyJointime.hasInstantiated; } photonLobbyJointime.value.set(idNum, { joinTime: Date.parse(gameLogDate), hasInstantiated, inVRMode: user.inVRMode, avatarEyeHeight: user.avatarEyeHeight, canModerateInstance: user.canModerateInstance, groupOnNameplate: user.groupOnNameplate, showGroupBadgeToOthers: user.showGroupBadgeToOthers, showSocialRank: user.showSocialRank, useImpostorAsFallback: user.useImpostorAsFallback, platform: user.platform }); photonUserJoin(idNum, user, gameLogDate); } break; case 42: // SetUserProperties var id42 = safeParseInt(data.Parameters[254]); var user42 = data.Parameters[245]; if (id42 === null || !user42 || !user42.user) break; parsePhotonUser(id42, user42.user, gameLogDate); if (user42.avatarDict) { parsePhotonAvatarChange( id42, user42.user, user42.avatarDict, gameLogDate ); parsePhotonAvatar(user42.avatarDict); } if (user42.favatarDict) { parsePhotonAvatar(user42.favatarDict); } if (typeof user42.groupOnNameplate !== 'undefined') { parsePhotonGroupChange( id42, user42.user, user42.groupOnNameplate, gameLogDate ); } var lobbyJointime = photonLobbyJointime.value.get(id42); photonLobbyJointime.value.set(id42, { hasInstantiated: true, ...lobbyJointime, inVRMode: user42.inVRMode, avatarEyeHeight: user42.avatarEyeHeight, canModerateInstance: user42.canModerateInstance, groupOnNameplate: user42.groupOnNameplate, showGroupBadgeToOthers: user42.showGroupBadgeToOthers, showSocialRank: user42.showSocialRank, useImpostorAsFallback: user42.useImpostorAsFallback, platform: user42.platform }); break; case 255: // Join const id255 = safeParseInt(data.Parameters[254]); if (id255 === null) break; if (typeof data.Parameters[249] !== 'undefined') { const u255 = data.Parameters[249]; if (u255 && u255.user) { parsePhotonUser(id255, u255.user, gameLogDate); if (u255.avatarDict) { parsePhotonAvatarChange( id255, u255.user, u255.avatarDict, gameLogDate ); parsePhotonAvatar(u255.avatarDict); } if (u255.favatarDict) { parsePhotonAvatar(u255.favatarDict); } if (typeof u255.groupOnNameplate !== 'undefined') { parsePhotonGroupChange( id255, u255.user, u255.groupOnNameplate, gameLogDate ); } } } parsePhotonLobbyIds(data.Parameters[252]); var hasInstantiated = false; if (photonLobbyCurrentUser.value === id255) { // fix current user hasInstantiated = true; } var ref = photonLobbyCurrent.value.get(id255); if (typeof ref !== 'undefined') { // fix for join event firing twice // fix instantiation happening out of order before join event hasInstantiated = ref.hasInstantiated; } if (data.Parameters[249]) { photonLobbyJointime.value.set(id255, { joinTime: Date.parse(gameLogDate), hasInstantiated, inVRMode: data.Parameters[249].inVRMode, avatarEyeHeight: data.Parameters[249].avatarEyeHeight, canModerateInstance: data.Parameters[249].canModerateInstance, groupOnNameplate: data.Parameters[249].groupOnNameplate, showGroupBadgeToOthers: data.Parameters[249].showGroupBadgeToOthers, showSocialRank: data.Parameters[249].showSocialRank, useImpostorAsFallback: data.Parameters[249].useImpostorAsFallback, platform: data.Parameters[249].platform }); photonUserJoin(id255, data.Parameters[249], gameLogDate); } startLobbyWatcherLoop(); break; case 254: // Leave var photonId = data.Parameters[254]; photonUserLeave(photonId, gameLogDate); photonLobbyCurrent.value.delete(photonId); photonLobbyLastModeration.value.delete(photonId); photonLobbyJointime.value.delete(photonId); photonEvent7List.value.delete(photonId); parsePhotonLobbyIds(data.Parameters[252]); if (typeof data.Parameters[203] !== 'undefined') { setPhotonLobbyMaster(data.Parameters[203], gameLogDate); } break; case 4: // Sync setPhotonLobbyMaster(data.Parameters[254], gameLogDate); break; case 33: // Moderation if (data.Parameters[245]['0'] === 21) { if (data.Parameters[245]['1']) { var photonId = data.Parameters[245]['1']; const block = data.Parameters[245]['10']; const mute = data.Parameters[245]['11']; var ref = photonLobby.value.get(photonId); if ( typeof ref !== 'undefined' && typeof ref.id !== 'undefined' ) { photonModerationUpdate( ref, photonId, block, mute, gameLogDate ); } else { moderationEventQueue.value.set(photonId, { block, mute, gameLogDate }); } } else { const blockArray = data.Parameters[245]['10']; const muteArray = data.Parameters[245]['11']; const idList = new Map(); blockArray.forEach((photonId1) => { if (muteArray.includes(photonId1)) { idList.set(photonId1, { isMute: true, isBlock: true }); } else { idList.set(photonId1, { isMute: false, isBlock: true }); } }); muteArray.forEach((photonId2) => { if (!idList.has(photonId2)) { idList.set(photonId2, { isMute: true, isBlock: false }); } }); idList.forEach(({ isMute, isBlock }, photonId3) => { const ref1 = photonLobby.value.get(photonId3); if ( typeof ref1 !== 'undefined' && typeof ref1.id !== 'undefined' ) { photonModerationUpdate( ref1, photonId3, isBlock, isMute, gameLogDate ); } else { moderationEventQueue.value.set(photonId3, { block: isBlock, mute: isMute, gameLogDate }); } }); } } else if ( data.Parameters[245]['0'] === 13 || data.Parameters[245]['0'] === 25 ) { let msg = data.Parameters[245]['2']; if ( typeof msg === 'string' && typeof data.Parameters[245]['14'] === 'object' ) { for (let prop in data.Parameters[245]['14']) { const value = data.Parameters[245]['14'][prop]; msg = msg.replace(`{{${prop}}}`, value); } } addEntryPhotonEvent({ photonId, text: msg, type: 'Moderation', color: 'yellow', created_at: gameLogDate }); } break; case 202: // Instantiate if (!photonLobby.value.has(data.Parameters[254])) { photonLobby.value.set(data.Parameters[254]); } if (!photonLobbyCurrent.value.has(data.Parameters[254])) { photonLobbyCurrent.value.set(data.Parameters[254]); } var lobbyJointime = photonLobbyJointime.value.get( data.Parameters[254] ); if (typeof lobbyJointime !== 'undefined') { photonLobbyJointime.value.set(data.Parameters[254], { ...lobbyJointime, hasInstantiated: true }); } else { photonLobbyJointime.value.set(data.Parameters[254], { joinTime: Date.parse(gameLogDate), hasInstantiated: true }); } break; case 43: // Chatbox Message var photonId = data.Parameters[254]; var text = data.Parameters[245]; if (photonLobbyCurrentUser.value === photonId) { return; } const lastMsg = photonLastChatBoxMsg.value.get(photonId); if (lastMsg === text) { return; } photonLastChatBoxMsg.value.set(photonId, text); var userId = getUserIdFromPhotonId(photonId); if ( chatboxUserBlacklist.value.has(userId) || checkChatboxBlacklist(text) ) { return; } addEntryPhotonEvent({ photonId, text, type: 'ChatBoxMessage', created_at: gameLogDate }); const entry = { userId, displayName: getDisplayNameFromPhotonId(photonId), created_at: gameLogDate, type: 'ChatBoxMessage', text }; notificationStore.queueGameLogNoty(entry); gameLogStore.addGameLog(entry); break; case 70: // Portal Spawn if (data.Parameters[245][0] === 20) { const portalId = data.Parameters[245][1]; const portalUserId = data.Parameters[245][2]; const shortName = data.Parameters[245][5]; const portalWorldName = data.Parameters[245][8].name; addPhotonPortalSpawn( gameLogDate, portalUserId, shortName, portalWorldName ); photonLobbyActivePortals.value.set(portalId, { userId: portalUserId, shortName, worldName: portalWorldName, created_at: Date.parse(gameLogDate), pendingLeave: 0 }); } else if (data.Parameters[245][0] === 21) { const portalId = data.Parameters[245][1]; const portalUserId = data.Parameters[245][2]; const playerCount = data.Parameters[245][3]; const shortName = data.Parameters[245][5]; const portalWorldName = ''; addPhotonPortalSpawn( gameLogDate, portalUserId, shortName, portalWorldName ); photonLobbyActivePortals.value.set(portalId, { userId: portalUserId, shortName, worldName: portalWorldName, created_at: Date.parse(gameLogDate), playerCount: 0, pendingLeave: 0 }); } else if (data.Parameters[245][0] === 22) { const portalId = data.Parameters[245][1]; let portalText = 'DeletedPortal'; const ref = photonLobbyActivePortals.value.get(portalId); if (typeof ref !== 'undefined') { const portalWorldName = ref.worldName; const playerCount = ref.playerCount; const time = timeToText( Date.parse(gameLogDate) - ref.created_at ); portalText = `DeletedPortal after ${time} with ${playerCount} players to "${portalWorldName}"`; } addEntryPhotonEvent({ text: portalText, type: 'DeletedPortal', created_at: gameLogDate }); photonLobbyActivePortals.value.delete(portalId); } else if (data.Parameters[245][0] === 23) { const portalId = data.Parameters[245][1]; const playerCount = data.Parameters[245][3]; const ref = photonLobbyActivePortals.value.get(portalId); if (typeof ref !== 'undefined') { ref.pendingLeave++; ref.playerCount = playerCount; } } else if (data.Parameters[245][0] === 24) { addEntryPhotonEvent({ text: 'PortalError failed to create portal', type: 'DeletedPortal', created_at: gameLogDate }); } break; case 71: // Spawn Emoji var photonId = data.Parameters[254]; if (photonId === photonLobbyCurrentUser.value) { return; } const type = data.Parameters[245][0]; let emojiName = ''; let imageUrl = ''; if (type === 0) { const emojiId = data.Parameters[245][2]; emojiName = photonEmojis[emojiId]; } else if (type === 1) { emojiName = 'Custom'; var fileId = data.Parameters[245][1]; imageUrl = `${AppDebug.endpointDomain}/file/${fileId}/1/`; } addEntryPhotonEvent({ photonId, text: emojiName, type: 'SpawnEmoji', created_at: gameLogDate, imageUrl, fileId }); break; } } function parseVRCEvent(json) { // VRC Event const datetime = json.dt; const eventData = json.VRCEventData; const senderId = eventData.Sender; if (AppDebug.debugPhotonLogging) { console.log('VrcEvent:', json); } if (eventData.EventName === '_SendOnSpawn') { return; } else if (eventData.EventType > 34) { const entry = { created_at: datetime, type: 'Event', data: `${getDisplayNameFromPhotonId( senderId )} called non existent RPC ${eventData.EventType}` }; addPhotonEventToGameLog(entry); return; } if (eventData.EventType === 14) { let type = 'Event'; if (eventData.EventName === 'ChangeVisibility') { if (eventData.Data[0] === true) { var text = 'EnableCamera'; } else if (eventData.Data[0] === false) { var text = 'DisableCamera'; } type = 'Camera'; } else if (eventData.EventName === 'PhotoCapture') { var text = 'PhotoCapture'; type = 'Camera'; } else if (eventData.EventName === 'TimerBloop') { var text = 'TimerBloop'; type = 'Camera'; } else if (eventData.EventName === 'ReloadAvatarNetworkedRPC') { var text = 'AvatarReset'; } else if (eventData.EventName === 'ReleaseBones') { var text = 'ResetPhysBones'; } else if (eventData.EventName === 'SpawnEmojiRPC') { // var text = this.oldPhotonEmojis[eventData.Data]; type = 'SpawnEmoji'; } else { let eventVrc = ''; if (eventData.Data && eventData.Data.length > 0) { eventVrc = ` ${JSON.stringify(eventData.Data).replace( /"([^(")"]+)":/g, '$1:' )}`; } var text = `${eventData.EventName}${eventVrc}`; } addEntryPhotonEvent({ photonId: senderId, text, type, created_at: datetime }); } else { let eventName = ''; if (eventData.EventName) { eventName = ` ${JSON.stringify(eventData.EventName).replace( /"([^(")"]+)":/g, '$1:' )}`; } if (AppDebug.debugPhotonLogging) { const displayName = getDisplayNameFromPhotonId(senderId); const feed = `RPC ${displayName} ${ photonEventType[eventData.EventType] }${eventName}`; console.log('VrcRpc:', feed); } } } // async function parsePhotonPortalSpawn( // created_at, // instanceId, // ref, // portalType, // shortName, // photonId // ) { // let worldName = shortName; // if (instanceId) { // worldName = await getWorldName(instanceId); // } // addEntryPhotonEvent({ // photonId, // text: `${portalType} PortalSpawn to ${worldName}`, // type: 'PortalSpawn', // shortName, // location: instanceId, // worldName, // created_at // }); // addPhotonEventToGameLog({ // created_at, // type: 'PortalSpawn', // displayName: ref.displayName, // location: locationStore.lastLocation.location, // userId: ref.id, // instanceId, // worldName // }); // } async function addPhotonPortalSpawn( gameLogDate, userId, shortName, worldName ) { const instance = await instanceRequest.getInstanceFromShortName({ shortName }); const location = instance.json.location; const L = parseLocation(location); let groupName = ''; if (L.groupId) { groupName = await getGroupName(L.groupId); } if (!worldName) { worldName = await getWorldName(location); } // var newShortName = instance.json.shortName; // var portalType = 'Secure'; // if (shortName === newShortName) { // portalType = 'Unlocked'; // } const _displayLocation = displayLocation( location, worldName, groupName ); addEntryPhotonEvent({ photonId: getPhotonIdFromUserId(userId), text: `PortalSpawn to ${_displayLocation}`, type: 'PortalSpawn', shortName, location, worldName, groupName, created_at: gameLogDate }); addPhotonEventToGameLog({ created_at: gameLogDate, type: 'PortalSpawn', displayName: getDisplayName(userId), location: locationStore.lastLocation.location, userId, instanceId: location, worldName, groupName }); } function addPhotonEventToGameLog(entry) { notificationStore.queueGameLogNoty(entry); gameLogStore.addGameLog(entry); if (entry.type === 'PortalSpawn') { database.addGamelogPortalSpawnToDatabase(entry); } else if (entry.type === 'Event') { database.addGamelogEventToDatabase(entry); } } function parsePhotonLobbyIds(lobbyIds) { if (!Array.isArray(lobbyIds)) { return; } lobbyIds.forEach((id) => { if (!photonLobby.value.has(id)) { photonLobby.value.set(id); } if (!photonLobbyCurrent.value.has(id)) { photonLobbyCurrent.value.set(id); } }); for (var id of photonLobbyCurrent.value.keys()) { if (!lobbyIds.includes(id)) { photonLobbyCurrent.value.delete(id); photonEvent7List.value.delete(id); } } } function setPhotonLobbyMaster(photonId, gameLogDate) { if (photonLobbyMaster.value !== photonId) { if (photonLobbyMaster.value !== 0) { addEntryPhotonEvent({ photonId, text: `Photon Master Migrate`, type: 'MasterMigrate', created_at: gameLogDate }); } photonLobbyMaster.value = photonId; } } async function parsePhotonUser(photonId, user, gameLogDate) { if (typeof user === 'undefined') { console.error('PhotonUser: user is undefined', photonId); return; } let tags = []; if (typeof user.tags !== 'undefined') { tags = user.tags; } let ref = userStore.cachedUsers.get(user.id); const photonUser = { id: user.id, displayName: user.displayName, developerType: user.developerType, profilePicOverride: user.profilePicOverride, currentAvatarImageUrl: user.currentAvatarImageUrl, currentAvatarThumbnailImageUrl: user.currentAvatarThumbnailImageUrl, userIcon: user.userIcon, last_platform: user.last_platform, allowAvatarCopying: user.allowAvatarCopying, status: user.status, statusDescription: user.statusDescription, bio: user.bio, tags }; photonLobby.value.set(photonId, photonUser); photonLobbyCurrent.value.set(photonId, photonUser); photonLobbyUserDataUpdate(photonId, photonUser, gameLogDate); const bias = Date.parse(gameLogDate) + 60 * 1000; // 1min if (bias > Date.now()) { if (typeof ref === 'undefined' || typeof ref.id === 'undefined') { try { const args = await userRequest.getUser({ userId: user.id }); ref = args.ref; } catch (err) { console.error(err); ref = photonUser; } } else if ( !ref.isFriend && locationStore.lastLocation.playerList.has(user.id) ) { let { joinTime } = locationStore.lastLocation.playerList.get( user.id ); if (!joinTime) { joinTime = Date.parse(gameLogDate); } ref.$location_at = joinTime; ref.$online_for = joinTime; } if ( typeof ref.id !== 'undefined' && ref.currentAvatarImageUrl !== user.currentAvatarImageUrl ) { userStore.applyUser({ ...ref, currentAvatarImageUrl: user.currentAvatarImageUrl, currentAvatarThumbnailImageUrl: user.currentAvatarThumbnailImageUrl }); } } if (typeof ref !== 'undefined' && typeof ref.id !== 'undefined') { photonLobby.value.set(photonId, ref); photonLobbyCurrent.value.set(photonId, ref); // check moderation queue if (moderationEventQueue.value.has(photonId)) { var { block, mute, gameLogDate } = moderationEventQueue.value.get(photonId); moderationEventQueue.value.delete(photonId); photonModerationUpdate(ref, photonId, block, mute, gameLogDate); } } } function photonLobbyUserDataUpdate(photonId, photonUser, gameLogDate) { const ref = photonLobbyUserData.value.get(photonId); if ( typeof ref !== 'undefined' && photonId !== photonLobbyCurrentUser.value && (photonUser.status !== ref.status || photonUser.statusDescription !== ref.statusDescription) ) { addEntryPhotonEvent({ photonId, type: 'ChangeStatus', status: photonUser.status, previousStatus: ref.status, statusDescription: replaceBioSymbols( photonUser.statusDescription ), previousStatusDescription: replaceBioSymbols( ref.statusDescription ), created_at: Date.parse(gameLogDate) }); } photonLobbyUserData.value.set(photonId, photonUser); } function photonUserJoin(photonId, user, gameLogDate) { if (photonId === photonLobbyCurrentUser.value) { return; } const avatar = user.avatarDict; avatar.name = replaceBioSymbols(avatar.name); avatar.description = replaceBioSymbols(avatar.description); let platform = ''; if (user.last_platform === 'android') { platform = 'Android'; } else if (user.last_platform === 'ios') { platform = 'iOS'; } else if (user.inVRMode) { platform = 'VR'; } else { platform = 'Desktop'; } photonUserSusieCheck(photonId, user, gameLogDate); checkVRChatCache(avatar).then((cacheInfo) => { let inCache = false; if (cacheInfo.Item1 > 0) { inCache = true; } addEntryPhotonEvent({ photonId, text: 'has joined', type: 'OnPlayerJoined', created_at: gameLogDate, avatar, inCache, platform }); }); } function photonUserSusieCheck(photonId, user, gameLogDate) { let text = ''; if (typeof user.modTag !== 'undefined') { text = `Moderator has joined ${user.modTag}`; } else if (user.isInvisible) { text = 'User joined invisible'; } if (text) { addEntryPhotonEvent({ photonId, text, type: 'Event', color: 'yellow', created_at: gameLogDate }); const entry = { created_at: new Date().toJSON(), type: 'Event', data: `${text} - ${getDisplayNameFromPhotonId( photonId )} (${getUserIdFromPhotonId(photonId)})` }; notificationStore.queueGameLogNoty(entry); gameLogStore.addGameLog(entry); database.addGamelogEventToDatabase(entry); } } function photonUserLeave(photonId, gameLogDate) { if (!photonLobbyCurrent.value.has(photonId)) { return; } let text = 'has left'; const lastEvent = photonEvent7List.value.get(parseInt(photonId, 10)); if (typeof lastEvent !== 'undefined') { const timeSinceLastEvent = Date.now() - Date.parse(lastEvent); if (timeSinceLastEvent > 10 * 1000) { // 10 seconds text = `has timed out after ${timeToText(timeSinceLastEvent)}`; } } photonLobbyActivePortals.value.forEach((portal) => { if (portal.pendingLeave > 0) { text = `has left through portal to "${portal.worldName}"`; portal.pendingLeave--; } }); addEntryPhotonEvent({ photonId, text, type: 'OnPlayerLeft', created_at: gameLogDate }); } function photonModerationUpdate(ref, photonId, block, mute, gameLogDate) { database.getModeration(ref.id).then((row) => { const lastType = photonLobbyLastModeration.value.get(photonId); let type = ''; let text = ''; if (block) { type = 'Blocked'; text = 'Blocked'; } else if (mute) { type = 'Muted'; text = 'Muted'; } if (row.userId) { if (!block && row.block) { type = 'Unblocked'; text = 'Unblocked'; } else if (!mute && row.mute) { type = 'Unmuted'; text = 'Unmuted'; } if (block === row.block && mute === row.mute) { // no change if (type && type !== lastType) { addEntryPhotonEvent({ photonId, text: `Moderation ${text}`, type: 'Moderation', color: 'yellow', created_at: gameLogDate }); } photonLobbyLastModeration.value.set(photonId, type); return; } } photonLobbyLastModeration.value.set(photonId, type); moderationAgainstTable.value.forEach((item) => { if (item.userId === ref.id && item.type === type) { removeFromArray(moderationAgainstTable.value, item); } }); if (type) { addEntryPhotonEvent({ photonId, text: `Moderation ${text}`, type: 'Moderation', color: 'yellow', created_at: gameLogDate }); const noty = { created_at: new Date().toJSON(), userId: ref.id, displayName: ref.displayName, type }; notificationStore.queueModerationNoty(noty); const entry = { created_at: gameLogDate, userId: ref.id, displayName: ref.displayName, type }; moderationAgainstTable.value.push(entry); } if (block || mute || block !== row.block || mute !== row.mute) { sharedFeedStore.updateSharedFeed(true); } if (block || mute) { database.setModeration({ userId: ref.id, updatedAt: gameLogDate, displayName: ref.displayName, block, mute }); } else if (row.block || row.mute) { database.deleteModeration(ref.id); } }); } function parsePhotonAvatarChange(photonId, user, avatar, gameLogDate) { if (typeof avatar === 'undefined') { return; } if (typeof user === 'undefined') { console.error('PhotonAvatarChange: user is undefined', photonId); return; } const oldAvatarId = photonLobbyAvatars.value.get(user.id); if ( oldAvatarId && oldAvatarId !== avatar.id && photonId !== photonLobbyCurrentUser.value ) { avatar.name = replaceBioSymbols(avatar.name); avatar.description = replaceBioSymbols(avatar.description); checkVRChatCache(avatar).then((cacheInfo) => { let inCache = false; if (cacheInfo.Item1 > 0) { inCache = true; } const entry = { created_at: new Date().toJSON(), type: 'AvatarChange', userId: user.id, displayName: user.displayName, name: avatar.name, description: avatar.description, avatarId: avatar.id, authorId: avatar.authorId, releaseStatus: avatar.releaseStatus, imageUrl: avatar.imageUrl, thumbnailImageUrl: avatar.thumbnailImageUrl }; notificationStore.queueGameLogNoty(entry); gameLogStore.addGameLog(entry); addEntryPhotonEvent({ photonId, displayName: user.displayName, userId: user.id, text: `ChangeAvatar ${avatar.name}`, type: 'ChangeAvatar', created_at: gameLogDate, avatar, inCache }); }); } photonLobbyAvatars.value.set(user.id, avatar.id); } async function parsePhotonGroupChange( photonId, user, groupId, gameLogDate ) { if ( typeof user === 'undefined' || !photonLobbyJointime.value.has(photonId) ) { return; } let { groupOnNameplate } = photonLobbyJointime.value.get(photonId); if ( typeof groupOnNameplate !== 'undefined' && groupOnNameplate !== groupId && photonId !== photonLobbyCurrentUser.value ) { const groupName = await getGroupName(groupId); const previousGroupName = await getGroupName(groupOnNameplate); addEntryPhotonEvent({ photonId, displayName: user.displayName, userId: user.id, text: `ChangeGroup ${groupName}`, type: 'ChangeGroup', created_at: gameLogDate, groupId, groupName, previousGroupId: groupOnNameplate, previousGroupName }); } } function parsePhotonAvatar(avatar) { if (typeof avatar === 'undefined' || typeof avatar.id === 'undefined') { console.warn('PhotonAvatar: avatar is undefined'); return; } let tags = []; let unityPackages = []; if (typeof avatar.tags !== 'undefined') { tags = avatar.tags; } if (typeof avatar.unityPackages !== 'undefined') { unityPackages = avatar.unityPackages; } if (!avatar.assetUrl && unityPackages.length > 0) { for (const unityPackage of unityPackages) { if ( unityPackage.variant && unityPackage.variant !== 'standard' && unityPackage.variant !== 'security' ) { continue; } if (unityPackage.platform === 'standalonewindows') { avatar.assetUrl = unityPackage.assetUrl; } } } avatarStore.applyAvatar({ id: avatar.id, authorId: avatar.authorId, authorName: avatar.authorName, updated_at: avatar.updated_at, description: avatar.description, imageUrl: avatar.imageUrl, thumbnailImageUrl: avatar.thumbnailImageUrl, name: avatar.name, releaseStatus: avatar.releaseStatus, version: avatar.version, tags, unityPackages }); } function safeParseInt(val) { const n = parseInt(val, 10); return Number.isFinite(n) ? n : null; } return { state, photonLoggingEnabled, photonEventOverlay, photonEventOverlayFilter, photonEventTableTypeOverlayFilter, timeoutHudOverlay, timeoutHudOverlayFilter, photonEventIcon, photonLobbyTimeoutThreshold, photonOverlayMessageTimeout, photonEventTableTypeFilter, photonEventTable, photonEventTablePrevious, chatboxUserBlacklist, chatboxBlacklist, photonEventTableFilter, photonLobby, photonLobbyMaster, photonLobbyCurrentUser, photonLobbyUserData, photonLobbyCurrent, photonLobbyAvatars, photonLobbyLastModeration, photonLobbyTimeout, photonLobbyJointime, photonLobbyActivePortals, photonEvent7List, photonLastEvent7List, photonLastChatBoxMsg, moderationEventQueue, setPhotonLoggingEnabled, setPhotonEventOverlay, setPhotonEventOverlayFilter, setPhotonEventTableTypeOverlayFilter, setTimeoutHudOverlay, setTimeoutHudOverlayFilter, getDisplayName, photonEventPulse, parseOperationResponse, saveEventOverlay, checkChatboxBlacklist, saveChatboxUserBlacklist, saveChatboxBlacklist, photonEventTableFilterChange, showUserFromPhotonId, promptPhotonOverlayMessageTimeout, promptPhotonLobbyTimeoutThreshold, photonLobbyWatcherLoopStop, parsePhotonEvent, parseVRCEvent, moderationAgainstTable }; });