import { reactive, ref, shallowRef, watch } from 'vue'; import { defineStore } from 'pinia'; import { toast } from 'vue-sonner'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; import dayjs from 'dayjs'; import { convertYoutubeTime, findUserByDisplayName, formatSeconds, gameLogSearchFilter, getGroupName, isRpcWorld, parseLocation, replaceBioSymbols } from '../shared/utils'; import { AppDebug } from '../service/appConfig'; import { database } from '../service/database'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useFriendStore } from './friend'; import { useGalleryStore } from './gallery'; import { useGameStore } from './game'; import { useGeneralSettingsStore } from './settings/general'; import { useInstanceStore } from './instance'; import { useLocationStore } from './location'; import { useModalStore } from './modal'; import { useNotificationStore } from './notification'; import { usePhotonStore } from './photon'; import { useSharedFeedStore } from './sharedFeed'; import { useUiStore } from './ui'; import { useUserStore } from './user'; import { useVrStore } from './vr'; import { useVrcxStore } from './vrcx'; import { userRequest } from '../api'; import { watchState } from '../service/watchState'; import configRepository from '../service/config'; import gameLogService from '../service/gamelog.js'; import * as workerTimers from 'worker-timers'; export const useGameLogStore = defineStore('GameLog', () => { const notificationStore = useNotificationStore(); const vrStore = useVrStore(); const locationStore = useLocationStore(); const friendStore = useFriendStore(); const instanceStore = useInstanceStore(); const userStore = useUserStore(); const uiStore = useUiStore(); const vrcxStore = useVrcxStore(); const advancedSettingsStore = useAdvancedSettingsStore(); const gameStore = useGameStore(); const generalSettingsStore = useGeneralSettingsStore(); const galleryStore = useGalleryStore(); const photonStore = usePhotonStore(); const sharedFeedStore = useSharedFeedStore(); const modalStore = useModalStore(); const router = useRouter(); const { t } = useI18n(); const state = reactive({ lastLocationAvatarList: new Map() }); const gameLogTableData = shallowRef([]); const gameLogTable = ref({ loading: false, search: '', filter: [], pageSize: 20, pageSizeLinked: true, vip: false }); const nowPlaying = ref({ url: '', name: '', length: 0, startTime: 0, offset: 0, elapsed: 0, percentage: 0, remainingText: '', playing: false, thumbnailUrl: '' }); const lastVideoUrl = ref(''); const lastResourceloadUrl = ref(''); watch( () => watchState.isLoggedIn, () => { gameLogTableData.value = []; }, { flush: 'sync' } ); watch( router.currentRoute, (value) => { if (value.name === 'game-log') { initGameLogTable(); } else { gameLogTableData.value = []; } }, { immediate: true } ); watch( () => watchState.isFavoritesLoaded, (isFavoritesLoaded) => { if (isFavoritesLoaded && gameLogTable.value.vip) { gameLogTableLookup(); // re-apply VIP filter after friends are loaded } } ); watch( () => watchState.isFriendsLoaded, (isFriendsLoaded) => { if (isFriendsLoaded) { tryLoadPlayerList(); } }, { flush: 'sync' } ); async function init() { gameLogTable.value.filter = JSON.parse( await configRepository.getString('VRCX_gameLogTableFilters', '[]') ); gameLogTable.value.vip = await configRepository.getBool( 'VRCX_gameLogTableVIPFilter', false ); } init(); function getGameLogCreatedAtTs(row) { const createdAtRaw = row?.created_at ?? row?.createdAt ?? row?.dt; if (typeof createdAtRaw === 'number') { const ts = createdAtRaw > 1_000_000_000_000 ? createdAtRaw : createdAtRaw * 1000; return Number.isFinite(ts) ? ts : 0; } const createdAt = typeof createdAtRaw === 'string' ? createdAtRaw : ''; const ts = dayjs(createdAt).valueOf(); return Number.isFinite(ts) ? ts : 0; } function compareGameLogRows(a, b) { const aTs = getGameLogCreatedAtTs(a); const bTs = getGameLogCreatedAtTs(b); if (aTs !== bTs) { return bTs - aTs; } const aRowId = typeof a?.rowId === 'number' ? a.rowId : 0; const bRowId = typeof b?.rowId === 'number' ? b.rowId : 0; if (aRowId !== bRowId) { return bRowId - aRowId; } const aUid = typeof a?.uid === 'string' ? a.uid : ''; const bUid = typeof b?.uid === 'string' ? b.uid : ''; return aUid < bUid ? 1 : aUid > bUid ? -1 : 0; } function insertGameLogSorted(entry) { const arr = gameLogTableData.value; if (arr.length === 0) { gameLogTableData.value = [entry]; return; } if (compareGameLogRows(entry, arr[0]) < 0) { gameLogTableData.value = [entry, ...arr]; return; } if (compareGameLogRows(entry, arr[arr.length - 1]) > 0) { gameLogTableData.value = [...arr, entry]; return; } for (let i = 1; i < arr.length; i++) { if (compareGameLogRows(entry, arr[i]) < 0) { gameLogTableData.value = [ ...arr.slice(0, i), entry, ...arr.slice(i) ]; return; } } gameLogTableData.value = [...arr, entry]; } function clearNowPlaying() { nowPlaying.value = { url: '', name: '', length: 0, startTime: 0, offset: 0, elapsed: 0, percentage: 0, remainingText: '', playing: false, thumbnailUrl: '' }; vrStore.updateVrNowPlaying(); } function setNowPlaying(data) { const ctx = structuredClone(data); if (nowPlaying.value.url !== ctx.videoUrl) { if (!ctx.userId && ctx.displayName) { ctx.userId = findUserByDisplayName( userStore.cachedUsers, ctx.displayName )?.id ?? ''; } notificationStore.queueGameLogNoty(ctx); addGameLog(ctx); database.addGamelogVideoPlayToDatabase(ctx); let displayName = ''; if (ctx.displayName) { displayName = ` (${ctx.displayName})`; } const name = `${ctx.videoName}${displayName}`; nowPlaying.value = { url: ctx.videoUrl, name, length: ctx.videoLength, startTime: Date.parse(ctx.created_at) / 1000, offset: ctx.videoPos, elapsed: 0, percentage: 0, remainingText: '', playing: false, thumbnailUrl: ctx.thumbnailUrl }; } else { nowPlaying.value = { ...nowPlaying.value, length: ctx.videoLength, offset: ctx.videoPos, elapsed: 0, percentage: 0, remainingText: '', thumbnailUrl: ctx.thumbnailUrl }; if (ctx.updatedAt && ctx.videoPos) { nowPlaying.value.startTime = Date.parse(ctx.updatedAt) / 1000 - ctx.videoPos; } else { nowPlaying.value.startTime = Date.parse(ctx.created_at) / 1000 - ctx.videoPos; } } vrStore.updateVrNowPlaying(); if (!nowPlaying.value.playing && ctx.videoLength > 0) { nowPlaying.value.playing = true; updateNowPlaying(); } } function updateNowPlaying() { const np = nowPlaying.value; if (!nowPlaying.value.playing) { return; } const now = Date.now() / 1000; np.elapsed = Math.round((now - np.startTime) * 10) / 10; if (np.elapsed >= np.length) { clearNowPlaying(); return; } np.remainingText = formatSeconds(np.length - np.elapsed); np.percentage = Math.round(((np.elapsed * 100) / np.length) * 10) / 10; vrStore.updateVrNowPlaying(); workerTimers.setTimeout(() => updateNowPlaying(), 1000); } async function tryLoadPlayerList() { // TODO: make this work again if (!gameStore.isGameRunning) { return; } console.log('Loading player list from game log...'); let ctx; let i; const data = await database.getGamelogDatabase(); if (data.length === 0) { return; } let length = 0; for (i = data.length - 1; i > -1; i--) { ctx = data[i]; if (ctx.type === 'Location') { locationStore.lastLocation = { date: Date.parse(ctx.created_at), location: ctx.location, name: ctx.worldName, playerList: new Map(), friendList: new Map() }; length = i; break; } } if (length > 0) { for (i = length + 1; i < data.length; i++) { ctx = data[i]; if (ctx.type === 'OnPlayerJoined') { if (!ctx.userId) { ctx.userId = findUserByDisplayName( userStore.cachedUsers, ctx.displayName )?.id ?? ''; } const userMap = { displayName: ctx.displayName, userId: ctx.userId, joinTime: Date.parse(ctx.created_at), lastAvatar: '' }; locationStore.lastLocation.playerList.set( ctx.userId, userMap ); if (friendStore.friends.has(ctx.userId)) { locationStore.lastLocation.friendList.set( ctx.userId, userMap ); } } if (ctx.type === 'OnPlayerLeft') { locationStore.lastLocation.playerList.delete(ctx.userId); locationStore.lastLocation.friendList.delete(ctx.userId); } } locationStore.lastLocation.playerList.forEach((ref1) => { if ( ref1.userId && typeof ref1.userId === 'string' && !userStore.cachedUsers.has(ref1.userId) ) { userRequest.getUser({ userId: ref1.userId }); } }); locationStore.updateCurrentUserLocation(); instanceStore.updateCurrentInstanceWorld(); vrStore.updateVRLastLocation(); instanceStore.getCurrentInstanceUserList(); userStore.applyUserDialogLocation(); instanceStore.applyWorldDialogInstances(); instanceStore.applyGroupDialogInstances(); } } function gameLogIsFriend(row) { if (typeof row.isFriend !== 'undefined') { return row.isFriend; } if (!row.userId) { return false; } return friendStore.friends.has(row.userId); } function gameLogIsFavorite(row) { if (typeof row.isFavorite !== 'undefined') { return row.isFavorite; } if (!row.userId) { return false; } return friendStore.localFavoriteFriends.has(row.userId); } async function gameLogTableLookup() { await configRepository.setString( 'VRCX_gameLogTableFilters', JSON.stringify(gameLogTable.value.filter) ); await configRepository.setBool( 'VRCX_gameLogTableVIPFilter', gameLogTable.value.vip ); gameLogTable.value.loading = true; try { let vipList = []; if (gameLogTable.value.vip) { vipList = Array.from(friendStore.localFavoriteFriends.values()); } const search = gameLogTable.value.search.trim(); let rows = []; if (search) { rows = await database.searchGameLogDatabase( search, gameLogTable.value.filter, vipList, vrcxStore.searchLimit ); } else { rows = await database.lookupGameLogDatabase( gameLogTable.value.filter, vipList ); } for (const row of rows) { row.isFriend = gameLogIsFriend(row); row.isFavorite = gameLogIsFavorite(row); } gameLogTableData.value = rows; } finally { gameLogTable.value.loading = false; } } function addGameLog(entry) { entry.isFriend = gameLogIsFriend(entry); entry.isFavorite = gameLogIsFavorite(entry); // If the VIP friend filter is enabled, logs from other friends will be ignored. if ( gameLogTable.value.vip && !friendStore.localFavoriteFriends.has(entry.userId) && (entry.type === 'OnPlayerJoined' || entry.type === 'OnPlayerLeft' || entry.type === 'VideoPlay' || entry.type === 'PortalSpawn' || entry.type === 'External') ) { return; } if ( entry.type === 'LocationDestination' || entry.type === 'AvatarChange' || entry.type === 'ChatBoxMessage' || (entry.userId === userStore.currentUser.id && (entry.type === 'OnPlayerJoined' || entry.type === 'OnPlayerLeft')) ) { return; } if ( gameLogTable.value.filter.length > 0 && !gameLogTable.value.filter.includes(entry.type) ) { return; } if (!gameLogSearch(entry)) { return; } insertGameLogSorted(entry); sweepGameLog(); uiStore.notifyMenu('game-log'); } async function addGamelogLocationToDatabase(input) { const groupName = await getGroupName(input.location); const entry = { ...input, groupName }; database.addGamelogLocationToDatabase(entry); } function gameLogSearch(row) { return gameLogSearchFilter(row, gameLogTable.value.search); } function sweepGameLog() { const j = gameLogTableData.value.length; if (j > vrcxStore.maxTableSize + 50) { gameLogTableData.value = gameLogTableData.value.slice(0, -50); } } function addGameLogEntry(gameLog, location) { let entry = undefined; if (advancedSettingsStore.gameLogDisabled) { return; } let userId = String(gameLog.userId || ''); if (!userId && gameLog.displayName) { userId = findUserByDisplayName( userStore.cachedUsers, gameLog.displayName )?.id ?? ''; } switch (gameLog.type) { case 'location-destination': if (gameStore.isGameRunning) { // needs to be added before OnPlayerLeft entries from LocationReset addGameLog({ created_at: gameLog.dt, type: 'LocationDestination', location: gameLog.location }); locationStore.lastLocationReset(gameLog.dt); locationStore.lastLocation.location = 'traveling'; locationStore.lastLocationDestination = gameLog.location; locationStore.lastLocationDestinationTime = Date.parse( gameLog.dt ); state.lastLocationAvatarList.clear(); instanceStore.removeQueuedInstance(gameLog.location); locationStore.updateCurrentUserLocation(); clearNowPlaying(); instanceStore.updateCurrentInstanceWorld(); userStore.applyUserDialogLocation(); instanceStore.applyWorldDialogInstances(); instanceStore.applyGroupDialogInstances(); } break; case 'location': instanceStore.addInstanceJoinHistory( locationStore.lastLocation.location, gameLog.dt ); const worldName = replaceBioSymbols(gameLog.worldName); if (gameStore.isGameRunning) { locationStore.lastLocationReset(gameLog.dt); clearNowPlaying(); locationStore.lastLocation = { date: Date.parse(gameLog.dt), location: gameLog.location, name: worldName, playerList: new Map(), friendList: new Map() }; instanceStore.removeQueuedInstance(gameLog.location); locationStore.updateCurrentUserLocation(); vrStore.updateVRLastLocation(); instanceStore.updateCurrentInstanceWorld(); userStore.applyUserDialogLocation(); instanceStore.applyWorldDialogInstances(); instanceStore.applyGroupDialogInstances(); } instanceStore.addInstanceJoinHistory( gameLog.location, gameLog.dt ); const L = parseLocation(gameLog.location); entry = { created_at: gameLog.dt, type: 'Location', location: gameLog.location, worldId: L.worldId, worldName, groupName: '', time: 0 }; getGroupName(gameLog.location).then((groupName) => { entry.groupName = groupName; }); addGamelogLocationToDatabase(entry); break; case 'player-joined': const joinTime = Date.parse(gameLog.dt); const userMap = { displayName: gameLog.displayName, userId, joinTime, lastAvatar: '' }; locationStore.lastLocation.playerList.set(userId, userMap); const ref = userStore.cachedUsers.get(userId); if (!userId) { console.error('Missing userId:', gameLog.displayName); } else if (userId === userStore.currentUser.id) { // skip } else if ( friendStore.friends.has(userId) && typeof ref !== 'undefined' ) { locationStore.lastLocation.friendList.set(userId, userMap); if ( ref.location !== locationStore.lastLocation.location && ref.travelingToLocation !== locationStore.lastLocation.location ) { // fix $location_at with private ref.$location_at = joinTime; } } else if (typeof ref !== 'undefined') { // set $location_at to join time if user isn't a friend ref.$location_at = joinTime; } else { if (AppDebug.debugGameLog || AppDebug.debugWebRequests) { console.log('Fetching user from gameLog:', userId); } userRequest.getUser({ userId }); } vrStore.updateVRLastLocation(); instanceStore.getCurrentInstanceUserList(); entry = { created_at: gameLog.dt, type: 'OnPlayerJoined', displayName: gameLog.displayName, location, userId, time: 0 }; database.addGamelogJoinLeaveToDatabase(entry); break; case 'player-left': const ref1 = locationStore.lastLocation.playerList.get(userId); if (typeof ref1 === 'undefined') { break; } const time = dayjs(gameLog.dt) - ref1.joinTime; locationStore.lastLocation.playerList.delete(userId); locationStore.lastLocation.friendList.delete(userId); state.lastLocationAvatarList.delete(gameLog.displayName); photonStore.photonLobbyAvatars.delete(userId); vrStore.updateVRLastLocation(); instanceStore.getCurrentInstanceUserList(); entry = { created_at: gameLog.dt, type: 'OnPlayerLeft', displayName: gameLog.displayName, location, userId, time }; database.addGamelogJoinLeaveToDatabase(entry); break; case 'portal-spawn': if (vrcxStore.ipcEnabled && gameStore.isGameRunning) { break; } entry = { created_at: gameLog.dt, type: 'PortalSpawn', location, displayName: '', userId: '', instanceId: '', worldName: '' }; database.addGamelogPortalSpawnToDatabase(entry); break; case 'video-play': gameLog.videoUrl = decodeURI(gameLog.videoUrl); if (lastVideoUrl.value === gameLog.videoUrl) { break; } lastVideoUrl.value = gameLog.videoUrl; addGameLogVideo(gameLog, location, userId); break; case 'video-sync': const timestamp = gameLog.timestamp.replace(/,/g, ''); if (nowPlaying.value.playing) { nowPlaying.value.offset = parseInt(timestamp, 10); } break; case 'resource-load-string': case 'resource-load-image': if ( !generalSettingsStore.logResourceLoad || lastResourceloadUrl.value === gameLog.resourceUrl ) { break; } lastResourceloadUrl.value = gameLog.resourceUrl; entry = { created_at: gameLog.dt, type: gameLog.type === 'resource-load-string' ? 'StringLoad' : 'ImageLoad', resourceUrl: gameLog.resourceUrl, location }; database.addGamelogResourceLoadToDatabase(entry); break; case 'screenshot': // entry = { // created_at: gameLog.dt, // type: 'Event', // data: `Screenshot Processed: ${gameLog.screenshotPath.replace( // /^.*[\\/]/, // '' // )}` // }; // database.addGamelogEventToDatabase(entry); vrcxStore.processScreenshot(gameLog.screenshotPath); break; case 'api-request': if (AppDebug.debugWebRequests) { console.log('API Request:', gameLog.url); } // const userId = ''; // try { // const url = new URL(gameLog.url); // const urlParams = new URLSearchParams(gameLog.url); // if (url.pathname.substring(0, 13) === '/api/1/users/') { // const pathArray = url.pathname.split('/'); // userId = pathArray[4]; // } else if (urlParams.has('userId')) { // userId = urlParams.get('userId'); // } // } catch (err) { // console.error(err); // } // if (!userId) { // break; // } if (advancedSettingsStore.saveInstanceEmoji) { try { // https://api.vrchat.cloud/api/1/user/usr_032383a7-748c-4fb2-94e4-bcb928e5de6b/inventory/inv_75781d65-92fe-4a80-a1ff-27ee6e843b08 const url = new URL(gameLog.url); if ( url.pathname.substring(0, 12) === '/api/1/user/' && url.pathname.includes('/inventory/inv_') ) { const pathArray = url.pathname.split('/'); const userId = pathArray[4]; const inventoryId = pathArray[6]; if (userId && inventoryId.length === 40) { galleryStore.queueCheckInstanceInventory( inventoryId, userId ); } } } catch (err) { console.error(err); } } if (advancedSettingsStore.saveInstancePrints) { try { let printId = ''; const url1 = new URL(gameLog.url); if ( url1.pathname.substring(0, 14) === '/api/1/prints/' ) { const pathArray = url1.pathname.split('/'); printId = pathArray[4]; } if (printId && printId.length === 41) { galleryStore.queueSavePrintToFile(printId); } } catch (err) { console.error(err); } } break; case 'avatar-change': if (!gameStore.isGameRunning) { break; } let avatarName = state.lastLocationAvatarList.get( gameLog.displayName ); if ( photonStore.photonLoggingEnabled || avatarName === gameLog.avatarName ) { break; } if (!avatarName) { avatarName = gameLog.avatarName; state.lastLocationAvatarList.set( gameLog.displayName, avatarName ); break; } avatarName = gameLog.avatarName; state.lastLocationAvatarList.set( gameLog.displayName, avatarName ); entry = { created_at: gameLog.dt, type: 'AvatarChange', userId, name: avatarName, displayName: gameLog.displayName }; break; case 'vrcx': // VideoPlay(PyPyDance) "https://jd.pypy.moe/api/v1/videos/jr1NX4Jo8GE.mp4",0.1001,239.606,"0905 : [J-POP] 【まなこ】金曜日のおはよう 踊ってみた (vernities)" const type = gameLog.data.substr(0, gameLog.data.indexOf(' ')); if (type === 'VideoPlay(PyPyDance)') { addGameLogPyPyDance(gameLog, location); } else if (type === 'VideoPlay(VRDancing)') { addGameLogVRDancing(gameLog, location); } else if (type === 'VideoPlay(ZuwaZuwaDance)') { addGameLogZuwaZuwaDance(gameLog, location); } else if (type === 'LSMedia') { addGameLogLSMedia(gameLog, location); } else if (type === 'VideoPlay(PopcornPalace)') { addGameLogPopcornPalace(gameLog, location); } break; case 'photon-id': if (!gameStore.isGameRunning || !watchState.isFriendsLoaded) { break; } const photonId = parseInt(gameLog.photonId, 10); const ref2 = photonStore.photonLobby.get(photonId); if (typeof ref2 === 'undefined') { const foundUser = findUserByDisplayName( userStore.cachedUsers, gameLog.displayName ); if (foundUser) { photonStore.photonLobby.set(photonId, foundUser); photonStore.photonLobbyCurrent.set(photonId, foundUser); } const ctx1 = { displayName: gameLog.displayName }; photonStore.photonLobby.set(photonId, ctx1); photonStore.photonLobbyCurrent.set(photonId, ctx1); instanceStore.getCurrentInstanceUserList(); } break; case 'notification': // entry = { // created_at: gameLog.dt, // type: 'Notification', // data: gameLog.json // }; break; case 'event': entry = { created_at: gameLog.dt, type: 'Event', data: gameLog.event }; database.addGamelogEventToDatabase(entry); break; case 'vrc-quit': if (!gameStore.isGameRunning) { break; } if (advancedSettingsStore.vrcQuitFix) { const bias = Date.parse(gameLog.dt) + 3000; if (bias < Date.now()) { console.log('QuitFix: Bias too low, not killing VRC'); break; } AppApi.QuitGame().then((processCount) => { if (processCount > 1) { console.log( 'QuitFix: More than 1 process running, not killing VRC' ); } else if (processCount === 1) { console.log('QuitFix: Killed VRC'); } else { console.log( 'QuitFix: Nothing to kill, no VRC process running' ); } }); } break; case 'openvr-init': gameStore.isGameNoVR = false; configRepository.setBool('isGameNoVR', gameStore.isGameNoVR); vrStore.updateOpenVR(); break; case 'desktop-mode': gameStore.isGameNoVR = true; configRepository.setBool('isGameNoVR', gameStore.isGameNoVR); vrStore.updateOpenVR(); break; case 'udon-exception': if (generalSettingsStore.udonExceptionLogging) { console.log('UdonException', gameLog.data); } // entry = { // created_at: gameLog.dt, // type: 'Event', // data: gameLog.data // }; // database.addGamelogEventToDatabase(entry); break; case 'sticker-spawn': if (!advancedSettingsStore.saveInstanceStickers) { break; } galleryStore.trySaveStickerToFile( gameLog.displayName, gameLog.userId, gameLog.inventoryId ); break; } if (typeof entry !== 'undefined') { sharedFeedStore.addEntry(entry); notificationStore.queueGameLogNoty(entry); addGameLog(entry); } } async function addGameLogVideo(gameLog, location, userId) { let url; const videoUrl = gameLog.videoUrl; let youtubeVideoId = ''; let videoId = ''; let videoName = ''; let videoLength = 0; let displayName = ''; let videoPos = 8; // video loading delay if (typeof gameLog.displayName !== 'undefined') { displayName = gameLog.displayName; } if (typeof gameLog.videoPos !== 'undefined') { videoPos = gameLog.videoPos; } if (!isRpcWorld(location) || gameLog.videoId === 'YouTube') { // skip PyPyDance and VRDancing videos try { url = new URL(videoUrl); if ( url.origin === 'https://t-ne.x0.to' || url.origin === 'https://nextnex.com' || url.origin === 'https://r.0cm.org' ) { url = new URL(url.searchParams.get('url')); } if (videoUrl.startsWith('https://u2b.cx/')) { url = new URL(videoUrl.substring(15)); } const id1 = url.pathname; const id2 = url.searchParams.get('v'); if (id1 && id1.length === 12) { // https://youtu.be/ youtubeVideoId = id1.substring(1, 12); } if (id1 && id1.length === 19) { // https://www.youtube.com/shorts/ youtubeVideoId = id1.substring(8, 19); } if (id2 && id2.length === 11) { // https://www.youtube.com/watch?v= // https://music.youtube.com/watch?v= youtubeVideoId = id2; } if (advancedSettingsStore.youTubeApi && youtubeVideoId) { const data = await advancedSettingsStore.lookupYouTubeVideo( youtubeVideoId ); if (data || data.pageInfo.totalResults !== 0) { videoId = 'YouTube'; videoName = data.items[0].snippet.title; videoLength = convertYoutubeTime( data.items[0].contentDetails.duration ); } } } catch { console.error(`Invalid URL: ${url}`); } const entry = { created_at: gameLog.dt, type: 'VideoPlay', videoUrl, videoId, videoName, videoLength, location, displayName, userId, videoPos }; setNowPlaying(entry); } } function addGameLogPyPyDance(gameLog, location) { const data = /VideoPlay\(PyPyDance\) "(.+?)",([\d.]+),([\d.]+),"(.*)"/g.exec( gameLog.data ); if (!data) { console.error('failed to parse', gameLog.data); return; } const videoUrl = data[1]; const videoPos = Number(data[2]); const videoLength = Number(data[3]); const title = data[4]; const bracketArray = title.split('('); const text1 = bracketArray.pop(); let displayName = text1.slice(0, -1); let text2 = bracketArray.join('('); let videoId = ''; if (text2 === 'Custom URL') { videoId = 'YouTube'; } else { videoId = text2.substr(0, text2.indexOf(':') - 1); text2 = text2.substr(text2.indexOf(':') + 2); } const videoName = text2.slice(0, -1); if (displayName === 'Random') { displayName = ''; } if (videoUrl === nowPlaying.value.url) { const entry = { updatedAt: gameLog.dt, videoUrl, videoLength, videoPos }; setNowPlaying(entry); return; } let userId = ''; if (displayName) { userId = findUserByDisplayName(userStore.cachedUsers, displayName)?.id ?? ''; } if (videoId === 'YouTube') { const entry1 = { dt: gameLog.dt, videoUrl, displayName, videoPos, videoId }; addGameLogVideo(entry1, location, userId); } else { const entry2 = { created_at: gameLog.dt, type: 'VideoPlay', videoUrl, videoId, videoName, videoLength, location, displayName, userId, videoPos }; setNowPlaying(entry2); } } function addGameLogVRDancing(gameLog, location) { const data = /VideoPlay\(VRDancing\) "(.+?)",([\d.]+),([\d.]+),(-?[\d.]+),"(.+?)","(.+?)"/g.exec( gameLog.data ); if (!data) { console.error('failed to parse', gameLog.data); return; } const videoUrl = data[1]; let videoPos = Number(data[2]); const videoLength = Number(data[3]); let videoId = data[4]; const displayName = data[5]; let videoName = data[6]; if (videoId === '-1') { videoId = 'YouTube'; } const videoNameIndex = videoName.indexOf('] '); if (videoNameIndex !== -1) { videoName = videoName.substring(videoNameIndex + 6); } if (videoPos === videoLength) { // ummm okay videoPos = 0; } if (videoUrl === nowPlaying.value.url) { const entry = { updatedAt: gameLog.dt, videoUrl, videoLength, videoPos }; setNowPlaying(entry); return; } let userId = ''; if (displayName) { userId = findUserByDisplayName(userStore.cachedUsers, displayName)?.id ?? ''; } if (videoId === 'YouTube') { const entry1 = { dt: gameLog.dt, videoUrl, displayName, videoPos, videoId }; addGameLogVideo(entry1, location, userId); } else { const entry2 = { created_at: gameLog.dt, type: 'VideoPlay', videoUrl, videoId, videoName, videoLength, location, displayName, userId, videoPos }; setNowPlaying(entry2); } } function addGameLogZuwaZuwaDance(gameLog, location) { const data = /VideoPlay\(ZuwaZuwaDance\) "(.+?)",([\d.]+),([\d.]+),(-?[\d.]+),"(.+?)","(.+?)"/g.exec( gameLog.data ); if (!data) { console.error('failed to parse', gameLog.data); return; } const videoUrl = data[1]; const videoPos = Number(data[2]); const videoLength = Number(data[3]); let videoId = data[4]; let displayName = data[5]; const videoName = data[6]; if (displayName === 'Random') { displayName = ''; } if (videoId === '9999') { videoId = 'YouTube'; } if (videoUrl === nowPlaying.value.url) { const entry = { updatedAt: gameLog.dt, videoUrl, videoLength, videoPos }; setNowPlaying(entry); return; } let userId = ''; if (displayName) { userId = findUserByDisplayName(userStore.cachedUsers, displayName)?.id ?? ''; } if (videoId === 'YouTube') { const entry1 = { dt: gameLog.dt, videoUrl, displayName, videoPos, videoId }; addGameLogVideo(entry1, location, userId); } else { const entry2 = { created_at: gameLog.dt, type: 'VideoPlay', videoUrl, videoId, videoName, videoLength, location, displayName, userId, videoPos }; setNowPlaying(entry2); } } function addGameLogLSMedia(gameLog, location) { // [VRCX] LSMedia 0,4268.981,Natsumi-sama,, // [VRCX] LSMedia 0,6298.292,Natsumi-sama,The Outfit (2022), 1080p const data = /LSMedia ([\d.]+),([\d.]+),(.+?),(.+?),(?=[^,]*$)/g.exec( gameLog.data ); if (!data) { return; } const videoPos = Number(data[1]); const videoLength = Number(data[2]); const displayName = data[3]; const videoName = replaceBioSymbols(data[4]); const videoUrl = videoName; const videoId = 'LSMedia'; if (videoUrl === nowPlaying.value.url) { const entry = { updatedAt: gameLog.dt, videoUrl, videoLength, videoPos }; setNowPlaying(entry); return; } let userId = ''; if (displayName) { userId = findUserByDisplayName(userStore.cachedUsers, displayName)?.id ?? ''; } const entry1 = { created_at: gameLog.dt, type: 'VideoPlay', videoUrl, videoId, videoName, videoLength, location, displayName, userId, videoPos }; setNowPlaying(entry1); } function addGameLogPopcornPalace(gameLog, location) { // [VRCX] VideoPlay(PopcornPalace) {"videoName": "How to Train Your Dragon - 2025-06-06", "videoPos": 37.28777, "videoLength": 11474.05, "thumbnailUrl": "", "displayName": "miner28_3", "isPaused": false, "is3D": false, "looping": false} let data = gameLog.data; if (!data) { return; } try { const j = data.indexOf('{'); data = JSON.parse(data.substring(j)); } catch (err) { console.error('Failed to parse PopcornPalace data:', err); return; } const videoPos = Number(data.videoPos); const videoLength = Number(data.videoLength); const displayName = data.displayName || ''; const videoName = data.videoName || ''; const videoUrl = videoName; const videoId = 'PopcornPalace'; const thumbnailUrl = data.thumbnailUrl || ''; if (!videoName) { clearNowPlaying(); return; } if (videoUrl === nowPlaying.value.url) { const entry = { updatedAt: gameLog.dt, videoUrl, videoLength, videoPos, thumbnailUrl }; setNowPlaying(entry); return; } let userId = ''; if (displayName) { userId = findUserByDisplayName(userStore.cachedUsers, displayName)?.id ?? ''; } const entry1 = { created_at: gameLog.dt, type: 'VideoPlay', videoUrl, videoId, videoName, videoLength, location, displayName, userId, videoPos, thumbnailUrl }; setNowPlaying(entry1); } async function getGameLogTable() { await database.initTables(); const dateTill = await database.getLastDateGameLogDatabase(); updateGameLog(dateTill); } async function updateGameLog(dateTill) { await gameLogService.setDateTill(dateTill); await new Promise((resolve) => { workerTimers.setTimeout(resolve, 10000); }); let location = ''; for (const gameLog of await gameLogService.getAll()) { if (gameLog.type === 'location') { location = gameLog.location; } addGameLogEntry(gameLog, location); } } // use in C# function addGameLogEvent(json) { const rawLogs = JSON.parse(json); const gameLog = gameLogService.parseRawGameLog( rawLogs[1], rawLogs[2], rawLogs.slice(3) ); if ( AppDebug.debugGameLog && gameLog.type !== 'photon-id' && gameLog.type !== 'api-request' && gameLog.type !== 'udon-exception' ) { console.log('gameLog:', gameLog); } addGameLogEntry(gameLog, locationStore.lastLocation.location); } async function disableGameLogDialog() { if (gameStore.isGameRunning) { toast.error(t('message.gamelog.vrchat_must_be_closed')); return; } if (!advancedSettingsStore.gameLogDisabled) { modalStore .confirm({ description: t('confirm.disable_gamelog'), title: t('confirm.title') }) .then(({ ok }) => { if (!ok) return; advancedSettingsStore.setGameLogDisabled(); }) .catch(() => {}); } else { advancedSettingsStore.setGameLogDisabled(); } } async function initGameLogTable() { gameLogTable.value.loading = true; const rows = await database.lookupGameLogDatabase( gameLogTable.value.filter, [] ); for (const row of rows) { row.isFriend = gameLogIsFriend(row); row.isFavorite = gameLogIsFavorite(row); } gameLogTableData.value = rows; gameLogTable.value.loading = false; } return { state, nowPlaying, gameLogTable, gameLogTableData, lastVideoUrl, lastResourceloadUrl, clearNowPlaying, tryLoadPlayerList, gameLogIsFriend, gameLogIsFavorite, gameLogTableLookup, addGameLog, addGamelogLocationToDatabase, getGameLogTable, addGameLogEvent, disableGameLogDialog }; });