import { reactive, ref, shallowReactive, watch } from 'vue'; import { defineStore } from 'pinia'; import { toast } from 'vue-sonner'; import dayjs from 'dayjs'; import { convertYoutubeTime, formatSeconds, 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 state = reactive({ lastLocationAvatarList: new Map() }); const gameLogTable = ref({ data: shallowReactive([]), 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, (isLoggedIn) => { gameLogTable.value.data.length = 0; if (isLoggedIn) { // wait for friends to load, silly but works setTimeout(() => { initGameLogTable(); }, 800); } }, { flush: 'sync' } ); 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 clearNowPlaying() { nowPlaying.value = { url: '', name: '', length: 0, startTime: 0, offset: 0, elapsed: 0, percentage: 0, remainingText: '', playing: false, thumbnailUrl: '' }; vrStore.updateVrNowPlaying(); } function setNowPlaying(ctx) { if (nowPlaying.value.url !== ctx.videoUrl) { if (!ctx.userId && ctx.displayName) { for (const ref of userStore.cachedUsers.values()) { if (ref.displayName === ctx.displayName) { ctx.userId = ref.id; break; } } } 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) { for (let ref of userStore.cachedUsers.values()) { if (ref.displayName === ctx.displayName) { ctx.userId = ref.id; break; } } } 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; } row.isFriend = friendStore.friends.has(row.userId); return row.isFriend; } function gameLogIsFavorite(row) { if (typeof row.isFavorite !== 'undefined') { return row.isFavorite; } if (!row.userId) { return false; } row.isFavorite = friendStore.localFavoriteFriends.has(row.userId); return row.isFavorite; } 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; let vipList = []; if (gameLogTable.value.vip) { vipList = Array.from(friendStore.localFavoriteFriends.values()); } const rows = await database.lookupGameLogDatabase( gameLogTable.value.search, gameLogTable.value.filter, vipList ); for (const row of rows) { row.isFriend = gameLogIsFriend(row); row.isFavorite = gameLogIsFavorite(row); } gameLogTable.value.data = shallowReactive(rows); 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; } gameLogTable.value.data.push(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) { const value = gameLogTable.value.search.toUpperCase(); if (!value) { return true; } if ( (value.startsWith('wrld_') || value.startsWith('grp_')) && String(row.location).toUpperCase().includes(value) ) { return true; } switch (row.type) { case 'Location': if (String(row.worldName).toUpperCase().includes(value)) { return true; } return false; case 'OnPlayerJoined': if (String(row.displayName).toUpperCase().includes(value)) { return true; } return false; case 'OnPlayerLeft': if (String(row.displayName).toUpperCase().includes(value)) { return true; } return false; case 'PortalSpawn': if (String(row.displayName).toUpperCase().includes(value)) { return true; } if (String(row.worldName).toUpperCase().includes(value)) { return true; } return false; case 'Event': if (String(row.data).toUpperCase().includes(value)) { return true; } return false; case 'External': if (String(row.message).toUpperCase().includes(value)) { return true; } if (String(row.displayName).toUpperCase().includes(value)) { return true; } return false; case 'VideoPlay': if (String(row.displayName).toUpperCase().includes(value)) { return true; } if (String(row.videoName).toUpperCase().includes(value)) { return true; } if (String(row.videoUrl).toUpperCase().includes(value)) { return true; } return false; case 'StringLoad': case 'ImageLoad': if (String(row.resourceUrl).toUpperCase().includes(value)) { return true; } return false; } return true; } function sweepGameLog() { const { data } = gameLogTable.value; const j = data.length; if (j > vrcxStore.maxTableSize + 50) { data.splice(0, 50); } } function addGameLogEntry(gameLog, location) { let entry = undefined; if (advancedSettingsStore.gameLogDisabled) { return; } let userId = String(gameLog.userId || ''); if (!userId && gameLog.displayName) { for (const ref of userStore.cachedUsers.values()) { if (ref.displayName === gameLog.displayName) { userId = ref.id; break; } } } 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') { for (const ctx of userStore.cachedUsers.values()) { if (ctx.displayName === gameLog.displayName) { photonStore.photonLobby.set(photonId, ctx); photonStore.photonLobbyCurrent.set(photonId, ctx); break; } } 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) { for (const ref of userStore.cachedUsers.values()) { if (ref.displayName === displayName) { userId = ref.id; break; } } } 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) { for (let ref of userStore.cachedUsers.values()) { if (ref.displayName === displayName) { userId = ref.id; break; } } } 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) { for (const ref of userStore.cachedUsers.values()) { if (ref.displayName === displayName) { userId = ref.id; break; } } } 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) { for (const ref of userStore.cachedUsers.values()) { if (ref.displayName === displayName) { userId = ref.id; break; } } } 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) { for (const ref of userStore.cachedUsers.values()) { if (ref.displayName === displayName) { userId = ref.id; break; } } } 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( 'VRChat needs to be closed before this option can be changed' ); return; } if (!advancedSettingsStore.gameLogDisabled) { modalStore .confirm({ description: 'Continue? Disable GameLog', title: 'Confirm' }) .then(({ ok }) => { if (!ok) return; advancedSettingsStore.setGameLogDisabled(); }) .catch(() => {}); } else { advancedSettingsStore.setGameLogDisabled(); } } async function initGameLogTable() { const rows = await database.lookupGameLogDatabase( gameLogTable.value.search, gameLogTable.value.filter, [] ); for (const row of rows) { row.isFriend = gameLogIsFriend(row); row.isFavorite = gameLogIsFavorite(row); } gameLogTable.value.data = shallowReactive(rows); } return { state, nowPlaying, gameLogTable, lastVideoUrl, lastResourceloadUrl, initGameLogTable, clearNowPlaying, tryLoadPlayerList, gameLogIsFriend, gameLogIsFavorite, gameLogTableLookup, addGameLog, addGamelogLocationToDatabase, getGameLogTable, addGameLogEvent, disableGameLogDialog }; });