/* * Vencord, a modification for Discord's desktop app * Copyright (c) 2023 Vendicated and contributors * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import "@equicordplugins/_misc/styles.css"; import { showNotification } from "@api/Notifications"; import { Devs } from "@utils/constants"; import { getTheme, Theme } from "@utils/discord"; import definePlugin from "@utils/types"; import { findByProps, findComponentByCodeLazy } from "@webpack"; import { Button, ChannelStore, FluxDispatcher, Forms, GuildChannelStore, NavigationRouter, RestAPI, Tooltip, UserStore } from "@webpack/common"; const isApp = typeof DiscordNative !== "undefined"; import "./style.css"; const QuestIcon = findComponentByCodeLazy("10.47a.76.76"); async function openCompleteQuestUI() { const ApplicationStreamingStore = findByProps("getStreamerActiveStreamMetadata"); const RunningGameStore = findByProps("getRunningGames"); const QuestsStore = findByProps("getQuest"); const quest = [...QuestsStore.quests.values()].find(x => x.id !== "1248385850622869556" && x.userStatus?.enrolledAt && !x.userStatus?.completedAt && new Date(x.config.expiresAt).getTime() > Date.now()); if (!quest) { showNotification({ title: "Quest Completer", body: "No Quests To Complete. Click to navigate to the quests tab", onClick() { NavigationRouter.transitionTo("/discovery/quests"); }, }); } else { const pid = Math.floor(Math.random() * 30000) + 1000; const theme = getTheme() === Theme.Light ? "light" : "dark"; const applicationId = quest.config.application.id; const applicationName = quest.config.application.name; const taskName = ["WATCH_VIDEO", "PLAY_ON_DESKTOP", "STREAM_ON_DESKTOP", "PLAY_ACTIVITY"].find(x => quest.config.taskConfig.tasks[x] != null); // @ts-ignore const secondsNeeded = quest.config.taskConfig.tasks[taskName].target; // @ts-ignore const secondsDone = quest.userStatus?.progress?.[taskName]?.value ?? 0; const icon = `https://cdn.discordapp.com/assets/quests/${quest.id}/${theme}/${quest.config.assets.gameTile}`; if (taskName === "WATCH_VIDEO") { const tolerance = 2, speed = 10; const diff = Math.floor((Date.now() - new Date(quest.userStatus.enrolledAt).getTime()) / 1000); const startingPoint = Math.min(Math.max(Math.ceil(secondsDone), diff), secondsNeeded); const fn = async () => { for (let i = startingPoint; i <= secondsNeeded; i += speed) { try { await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: Math.min(secondsNeeded, i + Math.random()) } }); } catch (ex) { console.log("Failed to send increment of", i, ex); } await new Promise(resolve => setTimeout(resolve, tolerance * 1000)); } if ((secondsNeeded - secondsDone) % speed !== 0) { await RestAPI.post({ url: `/quests/${quest.id}/video-progress`, body: { timestamp: secondsNeeded } }); showNotification({ title: `${applicationName} - Quest Completer`, body: "Quest Completed.", icon: icon, }); } }; fn(); showNotification({ title: `${applicationName} - Quest Completer`, body: `Wait for ${Math.ceil((secondsNeeded - startingPoint) / speed * tolerance)} more seconds.`, icon: icon, }); console.log(`Spoofing video for ${applicationName}.`); } else if (taskName === "PLAY_ON_DESKTOP") { if (!isApp) { showNotification({ title: `${applicationName} - Quest Completer`, body: `${applicationName}'s quest requires the desktop app.`, icon: icon, }); } RestAPI.get({ url: `/applications/public?application_ids=${applicationId}` }).then(res => { const appData = res.body[0]; const exeName = appData.executables.find(x => x.os === "win32").name.replace(">", ""); const fakeGame = { cmdLine: `C:\\Program Files\\${appData.name}\\${exeName}`, exeName, exePath: `c:/program files/${appData.name.toLowerCase()}/${exeName}`, hidden: false, isLauncher: false, id: applicationId, name: appData.name, pid: pid, pidPath: [pid], processName: appData.name, start: Date.now(), }; const realGames = RunningGameStore.getRunningGames(); const fakeGames = [fakeGame]; const realGetRunningGames = RunningGameStore.getRunningGames; const realGetGameForPID = RunningGameStore.getGameForPID; RunningGameStore.getRunningGames = () => fakeGames; RunningGameStore.getGameForPID = pid => fakeGames.find(x => x.pid === pid); FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: realGames, added: [fakeGame], games: fakeGames }); const fn = data => { const progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.PLAY_ON_DESKTOP.value); showNotification({ title: `${applicationName} - Quest Completer`, body: `Current progress: ${progress}/${secondsNeeded} seconds.`, icon: icon, }); if (progress >= secondsNeeded) { showNotification({ title: `${applicationName} - Quest Completer`, body: "Quest Completed.", icon: icon, }); RunningGameStore.getRunningGames = realGetRunningGames; RunningGameStore.getGameForPID = realGetGameForPID; FluxDispatcher.dispatch({ type: "RUNNING_GAMES_CHANGE", removed: [fakeGame], added: [], games: [] }); FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); } }; FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); console.log(`Spoofed your game to ${applicationName}.`); }); } else if (taskName === "STREAM_ON_DESKTOP") { if (!isApp) { showNotification({ title: `${applicationName} - Quest Completer`, body: `${applicationName}'s quest requires the desktop app.`, icon: icon, }); } const stream = ApplicationStreamingStore.getAnyStreamForUser(UserStore.getCurrentUser()?.id); if (!stream) { showNotification({ title: "You're not streaming - Quest Completer", body: `${applicationName} requires you to be streaming.\nPlease stream any window in vc.`, icon: icon, }); } showNotification({ title: `${applicationName} - Quest Completer`, body: "Remember that you need at least 1 other person to be in the vc!", icon: icon, }); const realFunc = ApplicationStreamingStore.getStreamerActiveStreamMetadata; ApplicationStreamingStore.getStreamerActiveStreamMetadata = () => ({ id: applicationId, pid, sourceName: null }); const fn = data => { const progress = quest.config.configVersion === 1 ? data.userStatus.streamProgressSeconds : Math.floor(data.userStatus.progress.STREAM_ON_DESKTOP.value); showNotification({ title: `${applicationName} - Quest Completer`, body: `Current progress: ${progress}/${secondsNeeded} seconds.`, icon: icon, }); if (progress >= secondsNeeded) { showNotification({ title: `${applicationName} - Quest Completer`, body: "Quest Completed.", icon: icon, }); ApplicationStreamingStore.getStreamerActiveStreamMetadata = realFunc; FluxDispatcher.unsubscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); } }; FluxDispatcher.subscribe("QUESTS_SEND_HEARTBEAT_SUCCESS", fn); console.log(`Spoofed your stream to ${applicationName}.`); } else if (taskName === "PLAY_ACTIVITY") { const channelId = ChannelStore.getSortedPrivateChannels()[0]?.id ?? Object.values(GuildChannelStore.getAllGuilds() as any[]).find(x => x != null && x.VOCAL.length > 0).VOCAL[0].channel.id; const streamKey = `call:${channelId}:1`; const fn = async () => { while (true) { const res = await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: false } }); const progress = res.body.progress.PLAY_ACTIVITY.value; showNotification({ title: `${applicationName} - Quest Completer`, body: `Current progress: ${progress}/${secondsNeeded} seconds.`, icon: icon, }); await new Promise(resolve => setTimeout(resolve, 20 * 1000)); if (progress >= secondsNeeded) { await RestAPI.post({ url: `/quests/${quest.id}/heartbeat`, body: { stream_key: streamKey, terminal: true } }); break; } } showNotification({ title: `${applicationName} - Quest Completer`, body: "Quest Completed.", icon: icon, }); }; fn(); } return; } } export default definePlugin({ name: "QuestCompleter", description: "A plugin to complete quests without having the game installed.", authors: [Devs.amia], settingsAboutComponent: () => <> Game Quests do not work on Equibop/Web Platforms. Only Video Quests do. , patches: [ { find: "AppTitleBar", replacement: { match: /(?<=trailing:.{0,70}\(\i\.Fragment,{children:\[.*?)\]/, replace: ",$self.renderQuestButton()]" } } ], renderQuestButton() { return ( {tooltipProps => ( )} ); }, });