Questify: Various changes. (#962)
* Lock quicker Video Quests behind setting. * More changes. * Semantics. * Semantics.
This commit is contained in:
parent
e9e42296c4
commit
a328c00fa3
4 changed files with 394 additions and 163 deletions
|
|
@ -21,12 +21,13 @@ import { JSX } from "react";
|
|||
|
||||
import { addIgnoredQuest, addRerenderCallback, autoFetchCompatible, fetchAndAlertQuests, maximumAutoFetchIntervalValue, minimumAutoFetchIntervalValue, questIsIgnored, removeIgnoredQuest, rerenderQuests, settings, startAutoFetchingQuests, stopAutoFetchingQuests, validateAndOverwriteIgnoredQuests } from "./settings";
|
||||
import { ActiveQuestIntervalsMap, ExcludedQuestMap, GuildlessServerListItem, Quest, QuestIcon, QuestMap, QuestStatus, QuestTaskType, RGB } from "./utils/components";
|
||||
import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getIgnoredQuestIDs, getQuestProgress, getQuestStatus, getQuestTarget, getQuestTask, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, setIgnoredQuestIDs, videoQuestLeeway, waitUntilEnrolled } from "./utils/misc";
|
||||
import { adjustRGB, decimalToRGB, fetchAndDispatchQuests, formatLowerBadge, getFormattedNow, getIgnoredQuestIDs, getQuestProgress, getQuestStatus, getQuestTarget, getQuestTask, isDarkish, leftClick, middleClick, normalizeQuestName, q, QuestifyLogger, questPath, QuestsStore, refreshQuest, reportPlayGameQuestProgress, reportVideoQuestProgress, rightClick, setIgnoredQuestIDs, waitUntilEnrolled } from "./utils/misc";
|
||||
|
||||
const AuthorizedAppsStore = findStoreLazy("AuthorizedAppsStore");
|
||||
let initialQuestDataFetched = false;
|
||||
const QuestifyNative = VencordNative.pluginHelpers.Questify as PluginNative<typeof import("./native")>;
|
||||
const patchedMobileQuests = new Set<string>();
|
||||
const manuallyStoppedQuestIDs = new Set<string>();
|
||||
export const activeQuestIntervals = new ActiveQuestIntervalsMap();
|
||||
|
||||
function questMenuUnignoreAllClicked(): void {
|
||||
|
|
@ -276,11 +277,12 @@ function QuestTileContextMenu(children: React.ReactNode[], props: { quest: any;
|
|||
const interval = activeQuestIntervals.get(props.quest.id);
|
||||
|
||||
if (interval) {
|
||||
manuallyStoppedQuestIDs.add(props.quest.id);
|
||||
QuestifyLogger.info(`[${getFormattedNow()}] Auto-Complete for Quest ${normalizeQuestName(props.quest.config.messages.questName)} stopped via context menu.`);
|
||||
resetQuestsToResume(props.quest);
|
||||
clearInterval(interval.progressTimeout);
|
||||
clearTimeout(interval.rerenderTimeout);
|
||||
activeQuestIntervals.delete(props.quest.id);
|
||||
resetQuestsToResume(props.quest);
|
||||
rerenderQuests();
|
||||
}
|
||||
}}
|
||||
|
|
@ -288,7 +290,7 @@ function QuestTileContextMenu(children: React.ReactNode[], props: { quest: any;
|
|||
<Menu.MenuItem
|
||||
id={q("start-auto-complete")}
|
||||
label="Start Auto-Complete"
|
||||
action={() => { processQuestForAutoComplete(props.quest); }}
|
||||
action={() => { processQuestForAutoComplete(props.quest, true); }}
|
||||
/>) : null
|
||||
}
|
||||
<Menu.MenuItem
|
||||
|
|
@ -571,9 +573,14 @@ function shouldPreloadQuestAssets(): boolean {
|
|||
}
|
||||
|
||||
async function startVideoProgressTracking(quest: Quest, target: { raw: number; adjusted: number; }): Promise<void> {
|
||||
quest = refreshQuest(quest);
|
||||
const questName = normalizeQuestName(quest.config.messages.questName);
|
||||
const task = getQuestTask(quest);
|
||||
const { completeVideoQuestsQuicker } = settings.store;
|
||||
const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null;
|
||||
const initialProgress = Math.floor(((new Date()).getTime() - (questEnrolledAt ?? new Date()).getTime()) / 1000) || 1;
|
||||
const initialProgress = completeVideoQuestsQuicker
|
||||
? Number(Math.max(1, ((new Date()).getTime() - (questEnrolledAt ?? new Date()).getTime()) / 1000).toFixed(6))
|
||||
: Math.max(0, getQuestProgress(quest, task) || 0);
|
||||
activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: initialProgress, type: "watch" });
|
||||
// Max up to ~25 seconds into the future can be reported.
|
||||
const { raw: reportTarget, adjusted: questTargetWithLeeway } = target;
|
||||
|
|
@ -599,14 +606,64 @@ async function startVideoProgressTracking(quest: Quest, target: { raw: number; a
|
|||
return;
|
||||
}
|
||||
|
||||
const reportEverySec = 10;
|
||||
let progressIntervalId: NodeJS.Timeout;
|
||||
const playbackTickMs = 250;
|
||||
const minimumReportDelta = 6;
|
||||
const maximumReportDelta = 8;
|
||||
const floatPrecision = 6;
|
||||
let progressIntervalId: NodeJS.Timeout | null = null;
|
||||
let renderIntervalId: NodeJS.Timeout | null = null;
|
||||
let maximumPlaybackTimestamp = Math.floor(initialProgress);
|
||||
let nextReportThreshold = 0;
|
||||
let hasReportedInitialProgress = currentProgressScaled <= 0;
|
||||
let lastPlaybackTickAt = performance.now();
|
||||
|
||||
function clampFloat(value: number): number {
|
||||
return Number(value.toFixed(floatPrecision));
|
||||
}
|
||||
|
||||
function randomBetween(min: number, max: number): number {
|
||||
return clampFloat(min + Math.random() * (max - min));
|
||||
}
|
||||
|
||||
function getNextReportThreshold(currentPlaybackPosition: number): number {
|
||||
return clampFloat(currentPlaybackPosition + randomBetween(minimumReportDelta, maximumReportDelta));
|
||||
}
|
||||
|
||||
function getProgressToReport(currentPlaybackPosition: number): number {
|
||||
const cappedProgress = clampFloat(Math.min(reportTarget, currentPlaybackPosition));
|
||||
const lowerBound = nextReportThreshold > 0
|
||||
? nextReportThreshold
|
||||
: clampFloat(Math.min(cappedProgress, 0.05));
|
||||
const overshootAvailable = clampFloat(Math.max(0, cappedProgress - lowerBound));
|
||||
|
||||
return overshootAvailable > 0
|
||||
? clampFloat(lowerBound + randomBetween(0, overshootAvailable))
|
||||
: lowerBound;
|
||||
}
|
||||
|
||||
function updateQuestIntervalProgress(progress: number): void {
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
|
||||
if (intervalData) {
|
||||
intervalData.progress = Math.floor(progress);
|
||||
}
|
||||
}
|
||||
|
||||
function clearTrackingTimers(): void {
|
||||
if (progressIntervalId) {
|
||||
clearTimeout(progressIntervalId);
|
||||
}
|
||||
|
||||
if (renderIntervalId) {
|
||||
clearInterval(renderIntervalId);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendComplete() {
|
||||
clearInterval(progressIntervalId);
|
||||
clearTimeout(renderIntervalId);
|
||||
const success = await reportVideoQuestProgress(quest, reportTarget, QuestifyLogger);
|
||||
clearTrackingTimers();
|
||||
const success = await reportVideoQuestProgress(quest, maximumPlaybackTimestamp, QuestifyLogger);
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
resetQuestsToResume(quest);
|
||||
|
||||
if (success) {
|
||||
QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`);
|
||||
|
|
@ -624,70 +681,104 @@ async function startVideoProgressTracking(quest: Quest, target: { raw: number; a
|
|||
}
|
||||
}
|
||||
|
||||
if (timeRemaining < reportEverySec) {
|
||||
const simulatedProgressToCover = reportTarget - currentProgressScaled;
|
||||
const speedFactor = timeRemaining > 0 ? simulatedProgressToCover / timeRemaining : 0;
|
||||
|
||||
async function scheduleNextPlaybackTick(): Promise<void> {
|
||||
progressIntervalId = setTimeout(async () => {
|
||||
await handleSendComplete();
|
||||
}, timeRemaining * 1000);
|
||||
} else {
|
||||
const simulatedProgressToCover = reportTarget - currentProgressScaled;
|
||||
const speedFactor = simulatedProgressToCover / timeRemaining;
|
||||
const progressIncrementPerInterval = reportEverySec * speedFactor;
|
||||
const numberOfIntervals = Math.floor(timeRemaining / reportEverySec);
|
||||
let intervalsRun = 0;
|
||||
|
||||
reportVideoQuestProgress(quest, initialProgress, QuestifyLogger);
|
||||
|
||||
progressIntervalId = setInterval(async () => {
|
||||
intervalsRun++;
|
||||
currentProgress += reportEverySec;
|
||||
currentProgressScaled += progressIncrementPerInterval;
|
||||
const progressToReport = Math.min(Math.floor(currentProgressScaled), reportTarget);
|
||||
|
||||
if (intervalsRun < numberOfIntervals || (currentProgress < reportTarget - videoQuestLeeway - (reportEverySec / 2))) {
|
||||
await reportVideoQuestProgress(quest, progressToReport, QuestifyLogger);
|
||||
if (!activeQuestIntervals.has(quest.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (intervalsRun >= numberOfIntervals) {
|
||||
clearInterval(progressIntervalId);
|
||||
const now = performance.now();
|
||||
const playbackElapsedSeconds = clampFloat((now - lastPlaybackTickAt) / 1000);
|
||||
lastPlaybackTickAt = now;
|
||||
|
||||
const timeSpentInIntervals = numberOfIntervals * reportEverySec;
|
||||
const finalWaitTime = Math.max(0, timeRemaining - timeSpentInIntervals);
|
||||
progressIntervalId = setTimeout(handleSendComplete, finalWaitTime * 1000);
|
||||
currentProgress = clampFloat(Math.min(questTargetWithLeeway, currentProgress + playbackElapsedSeconds));
|
||||
currentProgressScaled = clampFloat(Math.min(reportTarget, currentProgressScaled + playbackElapsedSeconds * speedFactor));
|
||||
maximumPlaybackTimestamp = Math.max(maximumPlaybackTimestamp, Math.floor(currentProgressScaled));
|
||||
updateQuestIntervalProgress(currentProgress);
|
||||
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
if (intervalData) intervalData.progressTimeout = progressIntervalId;
|
||||
if (hasReportedInitialProgress && currentProgressScaled >= nextReportThreshold && currentProgress < questTargetWithLeeway) {
|
||||
const progressToReport = getProgressToReport(currentProgressScaled);
|
||||
const reported = await reportVideoQuestProgress(quest, progressToReport, QuestifyLogger);
|
||||
|
||||
if (!reported) {
|
||||
clearTrackingTimers();
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to report progress for Quest ${questName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
nextReportThreshold = getNextReportThreshold(progressToReport);
|
||||
}
|
||||
}, reportEverySec * 1000);
|
||||
}
|
||||
|
||||
const renderIntervalId = setInterval(() => {
|
||||
if (currentProgress >= questTargetWithLeeway) {
|
||||
await handleSendComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
await scheduleNextPlaybackTick();
|
||||
}, playbackTickMs);
|
||||
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
|
||||
if (!!intervalData) {
|
||||
intervalData.progress += 1;
|
||||
} else {
|
||||
clearInterval(renderIntervalId);
|
||||
if (intervalData && progressIntervalId) {
|
||||
intervalData.progressTimeout = progressIntervalId;
|
||||
}
|
||||
}
|
||||
|
||||
rerenderQuests();
|
||||
}, 1000);
|
||||
updateQuestIntervalProgress(currentProgress);
|
||||
renderIntervalId = setInterval(() => rerenderQuests(), 1000);
|
||||
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
|
||||
if (intervalData) {
|
||||
intervalData.progressTimeout = progressIntervalId;
|
||||
intervalData.rerenderTimeout = renderIntervalId;
|
||||
}
|
||||
|
||||
if (timeRemaining <= 0) {
|
||||
await handleSendComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentProgressScaled > 0) {
|
||||
const initialProgressToReport = clampFloat(Math.min(reportTarget, currentProgressScaled));
|
||||
|
||||
void (async () => {
|
||||
const reported = await reportVideoQuestProgress(quest, initialProgressToReport, QuestifyLogger);
|
||||
|
||||
if (!activeQuestIntervals.has(quest.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reported) {
|
||||
clearTrackingTimers();
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to report initial progress for Quest ${questName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
hasReportedInitialProgress = true;
|
||||
nextReportThreshold = getNextReportThreshold(initialProgressToReport);
|
||||
})();
|
||||
}
|
||||
|
||||
lastPlaybackTickAt = performance.now();
|
||||
|
||||
await scheduleNextPlaybackTick();
|
||||
}
|
||||
|
||||
async function startPlayGameProgressTracking(quest: Quest, target: { raw: number; adjusted: number; }): Promise<void> {
|
||||
quest = refreshQuest(quest);
|
||||
const questName = normalizeQuestName(quest.config.messages.questName);
|
||||
const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null;
|
||||
const task = getQuestTask(quest);
|
||||
const questTarget = target.adjusted;
|
||||
const initialProgress = getQuestProgress(quest, task) || 0;
|
||||
const remaining = Math.max(0, questTarget - initialProgress);
|
||||
const heartbeatInterval = 20; // Heartbeats must be at most 2 minutes apart.
|
||||
const maximumHeartbeatDurationMs = 60 * 1000;
|
||||
const heartbeatBufferMs = 1 * 1000;
|
||||
activeQuestIntervals.set(quest.id, { progressTimeout: null as any, rerenderTimeout: null as any, progress: initialProgress, type: "play" });
|
||||
|
||||
QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} will be completed in the background in ${remaining} seconds.`);
|
||||
|
|
@ -708,76 +799,93 @@ async function startPlayGameProgressTracking(quest: Quest, target: { raw: number
|
|||
return;
|
||||
}
|
||||
|
||||
let progressTimeoutId: NodeJS.Timeout | null = null;
|
||||
let renderIntervalId: NodeJS.Timeout | null = null;
|
||||
|
||||
function getHeartbeatDurationMs(progress: number): number {
|
||||
const remainingMs = Math.max(0, (questTarget - progress) * 1000);
|
||||
|
||||
return remainingMs <= maximumHeartbeatDurationMs
|
||||
? remainingMs + heartbeatBufferMs
|
||||
: maximumHeartbeatDurationMs;
|
||||
}
|
||||
|
||||
function clearTrackingTimers(): void {
|
||||
if (progressTimeoutId) {
|
||||
clearTimeout(progressTimeoutId);
|
||||
}
|
||||
|
||||
if (renderIntervalId) {
|
||||
clearTimeout(renderIntervalId);
|
||||
}
|
||||
}
|
||||
|
||||
function handleQuestComplete(): void {
|
||||
clearTrackingTimers();
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`);
|
||||
|
||||
if (settings.store.notifyOnQuestComplete) {
|
||||
showNotification({
|
||||
title: "Quest Completed!",
|
||||
body: `The ${questName} Quest has completed.`,
|
||||
dismissOnClick: true,
|
||||
onClick: () => NavigationRouter.transitionTo(`${questPath}#${quest.id}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function scheduleNextHeartbeat(progress: number): Promise<void> {
|
||||
const heartbeatDurationMs = getHeartbeatDurationMs(progress);
|
||||
|
||||
progressTimeoutId = setTimeout(async () => {
|
||||
const result = await reportPlayGameQuestProgress(quest, false, QuestifyLogger, { attempts: 3, delay: 2500 });
|
||||
|
||||
if (result.progress === null) {
|
||||
clearTrackingTimers();
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to send heartbeat for Quest ${questName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
|
||||
if (intervalData) {
|
||||
intervalData.progress = result.progress;
|
||||
}
|
||||
|
||||
if (result.completed || result.progress >= questTarget) {
|
||||
handleQuestComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
await scheduleNextHeartbeat(result.progress);
|
||||
}, heartbeatDurationMs);
|
||||
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
|
||||
if (intervalData && progressTimeoutId) {
|
||||
intervalData.progressTimeout = progressTimeoutId;
|
||||
}
|
||||
}
|
||||
|
||||
const initial = await reportPlayGameQuestProgress(quest, false, QuestifyLogger, { attempts: 3, delay: 2500 });
|
||||
|
||||
const progressIntervalId = setInterval(async () => {
|
||||
const result = await reportPlayGameQuestProgress(quest, false, QuestifyLogger, { attempts: 3, delay: 2500 });
|
||||
if (initial.progress === null) {
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to send heartbeat for Quest ${questName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.progress === null) {
|
||||
clearInterval(progressIntervalId);
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to send heartbeat for Quest ${questName}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const isComplete = result.progress >= questTarget;
|
||||
const timeRemaining = questTarget - result.progress;
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
intervalData && (intervalData.progress = result.progress);
|
||||
|
||||
if (isComplete) {
|
||||
clearInterval(progressIntervalId);
|
||||
clearTimeout(renderIntervalId);
|
||||
const success = await reportPlayGameQuestProgress(quest, true, QuestifyLogger, { attempts: 3, delay: 2500 });
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
|
||||
if (success) {
|
||||
QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`);
|
||||
|
||||
if (settings.store.notifyOnQuestComplete) {
|
||||
showNotification({
|
||||
title: "Quest Completed!",
|
||||
body: `The ${questName} Quest has completed.`,
|
||||
dismissOnClick: true,
|
||||
onClick: () => NavigationRouter.transitionTo(`${questPath}#${quest.id}`),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to complete Quest ${questName}.`);
|
||||
}
|
||||
} else if (timeRemaining < heartbeatInterval) {
|
||||
clearInterval(progressIntervalId);
|
||||
clearTimeout(renderIntervalId);
|
||||
|
||||
setTimeout(async () => {
|
||||
const success = await reportPlayGameQuestProgress(quest, true, QuestifyLogger, { attempts: 3, delay: 2500 });
|
||||
activeQuestIntervals.delete(quest.id);
|
||||
|
||||
if (success) {
|
||||
QuestifyLogger.info(`[${getFormattedNow()}] Quest ${questName} completed.`);
|
||||
|
||||
if (settings.store.notifyOnQuestComplete) {
|
||||
showNotification({
|
||||
title: "Quest Completed!",
|
||||
body: `The ${questName} Quest has completed.`,
|
||||
dismissOnClick: true,
|
||||
onClick: () => NavigationRouter.transitionTo(`${questPath}#${quest.id}`),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
QuestifyLogger.error(`[${getFormattedNow()}] Failed to complete Quest ${questName}.`);
|
||||
}
|
||||
}, (timeRemaining + 1) * 1000);
|
||||
}
|
||||
}, heartbeatInterval * 1000);
|
||||
|
||||
const renderIntervalId = setInterval(() => {
|
||||
renderIntervalId = setInterval(() => {
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
|
||||
if (!!intervalData) {
|
||||
intervalData.progress += 1;
|
||||
} else {
|
||||
clearInterval(renderIntervalId);
|
||||
if (renderIntervalId) {
|
||||
clearInterval(renderIntervalId);
|
||||
}
|
||||
}
|
||||
|
||||
rerenderQuests();
|
||||
|
|
@ -787,9 +895,15 @@ async function startPlayGameProgressTracking(quest: Quest, target: { raw: number
|
|||
|
||||
if (intervalData) {
|
||||
intervalData.progress = initial.progress || initialProgress;
|
||||
intervalData.progressTimeout = progressIntervalId;
|
||||
intervalData.rerenderTimeout = renderIntervalId;
|
||||
}
|
||||
|
||||
if (initial.completed || initial.progress >= questTarget) {
|
||||
handleQuestComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
await scheduleNextHeartbeat(initial.progress);
|
||||
}
|
||||
|
||||
async function startAchievementActivityProgressTracking(quest: Quest, target: { raw: number; adjusted: number; }): Promise<void> {
|
||||
|
|
@ -912,7 +1026,7 @@ function canQuestAutoComplete(quest: Quest): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
function processQuestForAutoComplete(quest: Quest): boolean {
|
||||
function processQuestForAutoComplete(quest: Quest, force: boolean = false): boolean {
|
||||
const questName = normalizeQuestName(quest.config.messages.questName);
|
||||
|
||||
const task = getQuestTask(quest);
|
||||
|
|
@ -924,6 +1038,12 @@ function processQuestForAutoComplete(quest: Quest): boolean {
|
|||
const isAchievement = task?.type === QuestTaskType.ACHIEVEMENT_IN_ACTIVITY;
|
||||
const canAutoComplete = canQuestAutoComplete(quest);
|
||||
|
||||
if (force) {
|
||||
manuallyStoppedQuestIDs.delete(quest.id);
|
||||
} else if (manuallyStoppedQuestIDs.has(quest.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (quest.userStatus?.completedAt || existingInterval) {
|
||||
return false;
|
||||
} else if (!task) {
|
||||
|
|
@ -962,6 +1082,10 @@ function shouldDisableQuestAcceptedButton(quest: Quest): boolean | null {
|
|||
function getQuestUnacceptedButtonText(quest: Quest): string | null {
|
||||
const { completeGameQuestsInBackground, completeVideoQuestsInBackground, completeAchievementQuestsInBackground } = settings.store;
|
||||
|
||||
if (activeQuestIntervals.has(quest.id) || !!quest.userStatus?.enrolledAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const task = getQuestTask(quest);
|
||||
const { adjusted: target } = getQuestTarget(task);
|
||||
const targetFormatted = `${String(Math.floor(target / 60)).padStart(2, "0")}:${String(target % 60).padStart(2, "0")}`;
|
||||
|
|
@ -980,10 +1104,12 @@ function getQuestUnacceptedButtonText(quest: Quest): string | null {
|
|||
}
|
||||
|
||||
function getQuestAcceptedButtonText(quest: Quest, prepositional: boolean = false): string | null {
|
||||
const { completeGameQuestsInBackground, completeVideoQuestsInBackground, completeAchievementQuestsInBackground } = settings.store;
|
||||
const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null;
|
||||
const { completeVideoQuestsQuicker, completeGameQuestsInBackground, completeVideoQuestsInBackground, completeAchievementQuestsInBackground } = settings.store;
|
||||
|
||||
quest = refreshQuest(quest);
|
||||
const task = getQuestTask(quest);
|
||||
const intervalData = activeQuestIntervals.get(quest.id);
|
||||
const questEnrolledAt = quest.userStatus?.enrolledAt ? new Date(quest.userStatus.enrolledAt) : null;
|
||||
|
||||
const isWatch = task?.type === QuestTaskType.WATCH_VIDEO || task?.type === QuestTaskType.WATCH_VIDEO_ON_MOBILE;
|
||||
const isPlay = task?.type === QuestTaskType.PLAY_ON_DESKTOP || task?.type === QuestTaskType.PLAY_ON_XBOX || task?.type === QuestTaskType.PLAY_ON_PLAYSTATION || task?.type === QuestTaskType.PLAY_ACTIVITY;
|
||||
|
|
@ -992,10 +1118,14 @@ function getQuestAcceptedButtonText(quest: Quest, prepositional: boolean = false
|
|||
if (questEnrolledAt) {
|
||||
if (((isPlay && completeGameQuestsInBackground && IS_DISCORD_DESKTOP) || (isWatch && completeVideoQuestsInBackground))) {
|
||||
const { adjusted: durationWithLeeway } = getQuestTarget(task);
|
||||
const currentProgress = getQuestProgress(quest, task) || 0;
|
||||
const currentProgress = getQuestProgress(quest, task) ?? 0;
|
||||
const progress = Math.min(currentProgress, durationWithLeeway);
|
||||
const timeRemaining = Math.max(0, durationWithLeeway - progress);
|
||||
const canCompleteImmediately = isWatch && questEnrolledAt && ((new Date().getTime() - questEnrolledAt.getTime()) / 1000) >= durationWithLeeway;
|
||||
const canCompleteImmediately = isWatch && (
|
||||
completeVideoQuestsQuicker
|
||||
? !!questEnrolledAt && ((new Date().getTime() - questEnrolledAt.getTime()) / 1000) >= durationWithLeeway
|
||||
: !timeRemaining
|
||||
);
|
||||
const progressFormatted = `${String(Math.floor(timeRemaining / 60)).padStart(2, "0")}:${String(timeRemaining % 60).padStart(2, "0")}`;
|
||||
const progressFormattedAsPreposition = timeRemaining >= 60
|
||||
? `${Math.floor(timeRemaining / 60)}m ${timeRemaining % 60}s`
|
||||
|
|
@ -1023,6 +1153,7 @@ function getQuestAcceptedButtonText(quest: Quest, prepositional: boolean = false
|
|||
function getQuestPanelPercentComplete({ quest, percentCompleteText }: { quest: Quest; percentCompleteText?: string; }): { percentComplete: number; } | { percentComplete: number; percentCompleteText: string; } | null {
|
||||
if (!quest) { return null; }
|
||||
|
||||
quest = refreshQuest(quest);
|
||||
const task = getQuestTask(quest);
|
||||
|
||||
if (!task) { return null; }
|
||||
|
|
@ -1187,7 +1318,7 @@ function getQuestAcceptedButtonProps(quest: Quest, text: string, disabled: boole
|
|||
return {
|
||||
disabled: shouldDisableQuestAcceptedButton(quest) ?? disabled,
|
||||
text: getQuestAcceptedButtonText(quest) ?? text,
|
||||
onClick: () => { const startingAutocomplete = processQuestForAutoComplete(quest); !startingAutocomplete && onClick ? onClick() : null; },
|
||||
onClick: () => { const startingAutocomplete = processQuestForAutoComplete(quest, true); !startingAutocomplete && onClick ? onClick() : null; },
|
||||
icon: () => { }
|
||||
};
|
||||
}
|
||||
|
|
@ -1198,7 +1329,11 @@ function resetQuestsToResume(quest?: Quest): void {
|
|||
settings.store.resumeQuestIDs.watch = settings.store.resumeQuestIDs.watch.filter(id => id !== quest.id);
|
||||
settings.store.resumeQuestIDs.achievement = settings.store.resumeQuestIDs.achievement.filter(id => id !== quest.id);
|
||||
} else {
|
||||
settings.store.resumeQuestIDs = settings.def.resumeQuestIDs.default;
|
||||
settings.store.resumeQuestIDs = {
|
||||
watch: [],
|
||||
play: [],
|
||||
achievement: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1612,14 +1747,14 @@ export default definePlugin({
|
|||
{
|
||||
// Resume Video Quest.
|
||||
match: /(tooltipText:\i.intl.string\(\i.\i.\i\),onClick:\(\)=>)(\(0,\i.\i\)\({quest)/,
|
||||
replace: "$1!$self.processQuestForAutoComplete(arguments[0].quest)&&$2"
|
||||
replace: "$1!$self.processQuestForAutoComplete(arguments[0].quest,true)&&$2"
|
||||
},
|
||||
{
|
||||
// Start Play Game and Play Activity Quests.
|
||||
// Video Quests are handled in the next patch group.
|
||||
// Also set the unaccepted button text to "Complete".
|
||||
match: /(\i,tooltipText:null,onClick:async\(\)=>{)/,
|
||||
replace: "$self.getQuestUnacceptedButtonText(arguments[0].quest)??$1const startingAutoComplete=arguments[0].isVideoQuest?false:$self.processQuestForAutoComplete(arguments[0].quest);"
|
||||
replace: "$self.getQuestUnacceptedButtonText(arguments[0].quest)??$1const startingAutoComplete=arguments[0].isVideoQuest?false:$self.processQuestForAutoComplete(arguments[0].quest,true);"
|
||||
},
|
||||
{
|
||||
// Set the accepted button text to "Complete", "Completing", or "Resume" based on progress.
|
||||
|
|
@ -1649,11 +1784,11 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /(?=let{quest:)/,
|
||||
replace: "const questifyText=$self.getQuestUnacceptedButtonText(arguments[0].quest);"
|
||||
replace: "const questifyText=$self.getQuestUnacceptedButtonText(arguments[0].quest)??$self.getQuestAcceptedButtonText(arguments[0].quest);"
|
||||
},
|
||||
{
|
||||
match: /(?<=}\),)(\i\?\.\(\))/,
|
||||
replace: "!$self.processQuestForAutoComplete(arguments[0].quest)&&($1)"
|
||||
replace: "!$self.processQuestForAutoComplete(arguments[0].quest,true)&&($1)"
|
||||
},
|
||||
{
|
||||
match: /(?<=,text:)(\i),icon:\i/,
|
||||
|
|
@ -1667,7 +1802,7 @@ export default definePlugin({
|
|||
replacement: [
|
||||
{
|
||||
match: /(?=let{quest:)/,
|
||||
replace: "const questifyText=$self.getQuestUnacceptedButtonText(arguments[0].quest);"
|
||||
replace: "const questifyText=$self.getQuestUnacceptedButtonText(arguments[0].quest)??$self.getQuestAcceptedButtonText(arguments[0].quest);"
|
||||
},
|
||||
{
|
||||
match: /(?<=,text:)(\i)/g,
|
||||
|
|
@ -1675,7 +1810,7 @@ export default definePlugin({
|
|||
},
|
||||
{
|
||||
match: /(?<="primary",onClick:)(\i)/,
|
||||
replace: "!$self.processQuestForAutoComplete(arguments[0].quest)&&$1"
|
||||
replace: "()=>{!$self.processQuestForAutoComplete(arguments[0].quest,true)&&$1}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -1696,7 +1831,7 @@ export default definePlugin({
|
|||
{
|
||||
// Initial and subsequent select drop down for picking or changing a platform.
|
||||
match: /(select:)(\i)(,serialize:\i=>{)/g,
|
||||
replace: "$1(platform)=>{$self.processQuestForAutoComplete(arguments[0].quest),$2(platform)}$3"
|
||||
replace: "$1(platform)=>{$self.processQuestForAutoComplete(arguments[0].quest,true),$2(platform)}$3"
|
||||
},
|
||||
{
|
||||
// The Quest Accepted button is disabled by default. If the user reloads the client, they need a way
|
||||
|
|
|
|||
|
|
@ -686,6 +686,7 @@ function DisableQuestsSetting(): JSX.Element {
|
|||
disableFriendsListActiveNowPromotion,
|
||||
disableMembersListActivelyPlayingIcon,
|
||||
makeMobileQuestsDesktopCompatible,
|
||||
completeVideoQuestsQuicker,
|
||||
completeVideoQuestsInBackground,
|
||||
completeGameQuestsInBackground,
|
||||
completeAchievementQuestsInBackground,
|
||||
|
|
@ -702,6 +703,7 @@ function DisableQuestsSetting(): JSX.Element {
|
|||
"disableFriendsListActiveNowPromotion",
|
||||
"disableMembersListActivelyPlayingIcon",
|
||||
"makeMobileQuestsDesktopCompatible",
|
||||
"completeVideoQuestsQuicker",
|
||||
"completeVideoQuestsInBackground",
|
||||
"completeGameQuestsInBackground",
|
||||
"completeAchievementQuestsInBackground",
|
||||
|
|
@ -722,6 +724,7 @@ function DisableQuestsSetting(): JSX.Element {
|
|||
{ label: "Complete Watch Video Quests in Background", value: "video-quests-background", selected: completeVideoQuestsInBackground, type: "modification" },
|
||||
{ label: "Complete Play Game/Activity Quests in Background", value: "game-quests-background", selected: completeGameQuestsInBackground, type: "modification" },
|
||||
{ label: "Complete Task in Activity Quests in Background", value: "achievement-quests-background", selected: completeAchievementQuestsInBackground, type: "modification" },
|
||||
{ label: "Complete Watch Video Quests Quicker", value: "video-quests-quicker", selected: completeVideoQuestsQuicker, type: "modification" },
|
||||
{ label: "Make Mobile Quests Desktop Compatible", value: "mobile-desktop-compatible", selected: makeMobileQuestsDesktopCompatible, type: "modification" },
|
||||
{ label: "Notify on Auto-Complete", value: "notify-on-complete", selected: notifyOnQuestComplete, type: "modification" },
|
||||
];
|
||||
|
|
@ -752,6 +755,7 @@ function DisableQuestsSetting(): JSX.Element {
|
|||
settings.store.disableFriendsListActiveNowPromotion = enabled.includes("friends-list");
|
||||
settings.store.disableMembersListActivelyPlayingIcon = enabled.includes("members-list");
|
||||
settings.store.makeMobileQuestsDesktopCompatible = enabled.includes("mobile-desktop-compatible");
|
||||
settings.store.completeVideoQuestsQuicker = enabled.includes("video-quests-quicker");
|
||||
settings.store.completeVideoQuestsInBackground = enabled.includes("video-quests-background");
|
||||
settings.store.completeGameQuestsInBackground = enabled.includes("game-quests-background");
|
||||
settings.store.completeAchievementQuestsInBackground = enabled.includes("achievement-quests-background");
|
||||
|
|
@ -824,6 +828,10 @@ function DisableQuestsSetting(): JSX.Element {
|
|||
The <span className={q("inline-code-block")}>Complete Task in Activity Quests in Background</span> option
|
||||
will immediately mark Task in Activity Quests as completed when started.
|
||||
<br /><br />
|
||||
The <span className={q("inline-code-block")}>Complete Video Quests Quicker</span> option
|
||||
uses Discord's progress leeway and elapsed enrollment time for Video Quests. When disabled, Video
|
||||
Quests wait for the full duration at 1x speed, which may appear less suspicious to their systems.
|
||||
<br /><br />
|
||||
You still must start the Quests manually. The first click will start the Quests in the background.
|
||||
For Video Quests, subsequent clicks will open the video modal as normal. For Activity Quests, subsequent clicks
|
||||
will open the activity as normal. To abort the Quests, you can open the context menu on the Quest tile and
|
||||
|
|
@ -1694,6 +1702,15 @@ export const settings = definePluginSettings({
|
|||
default: true,
|
||||
hidden: true
|
||||
},
|
||||
completeVideoQuestsQuicker: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Use Discord's progress leeway and elapsed enrollment time for Video Quest auto-completion.",
|
||||
default: false,
|
||||
hidden: true,
|
||||
onChange: () => {
|
||||
rerenderQuests();
|
||||
}
|
||||
},
|
||||
completeVideoQuestsInBackground: {
|
||||
type: OptionType.BOOLEAN,
|
||||
description: "Complete Video Quests in the background after the video duration has passed.",
|
||||
|
|
|
|||
|
|
@ -22,13 +22,28 @@ export const QuestTile = findComponentByCodeLazy(".rowIndex,trackGuildAndChannel
|
|||
|
||||
export class ActiveQuestIntervalsMap extends Map<string, { progressTimeout: NodeJS.Timeout; rerenderTimeout: NodeJS.Timeout; progress: number; type: string; }> {
|
||||
set(key: string, value: { progressTimeout: NodeJS.Timeout; rerenderTimeout: NodeJS.Timeout; progress: number; type: string; }): this {
|
||||
const { resumeQuestIDs } = settings.store;
|
||||
|
||||
settings.store.resumeQuestIDs = {
|
||||
watch: resumeQuestIDs.watch.filter((id: string) => id !== key),
|
||||
play: resumeQuestIDs.play.filter((id: string) => id !== key),
|
||||
achievement: resumeQuestIDs.achievement.filter((id: string) => id !== key),
|
||||
};
|
||||
|
||||
settings.store.resumeQuestIDs[value.type].push(key);
|
||||
|
||||
return super.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: string): boolean {
|
||||
const types = Object.keys(settings.def.resumeQuestIDs.default);
|
||||
for (const type of types) { settings.store.resumeQuestIDs[type] = settings.store.resumeQuestIDs[type].filter((id: string) => id !== key); }
|
||||
const { resumeQuestIDs } = settings.store;
|
||||
|
||||
settings.store.resumeQuestIDs = {
|
||||
watch: resumeQuestIDs.watch.filter((id: string) => id !== key),
|
||||
play: resumeQuestIDs.play.filter((id: string) => id !== key),
|
||||
achievement: resumeQuestIDs.achievement.filter((id: string) => id !== key),
|
||||
};
|
||||
|
||||
return super.delete(key);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { classNameFactory } from "@utils/css";
|
||||
import { Logger } from "@utils/Logger";
|
||||
import { findByPropsLazy } from "@webpack";
|
||||
import { findByCodeLazy, findByPropsLazy } from "@webpack";
|
||||
import { FluxDispatcher, RestAPI, UserStore } from "@webpack/common";
|
||||
|
||||
import { activeQuestIntervals } from "..";
|
||||
|
|
@ -16,12 +16,19 @@ import { Quest, QuestStatus, QuestTask, QuestTaskType, RGB } from "./components"
|
|||
export const q = classNameFactory("questify-");
|
||||
export const QuestifyLogger = new Logger("Questify");
|
||||
export const QuestsStore = findByPropsLazy("getQuest");
|
||||
export const reportProgress = findByCodeLazy(".QUESTS_VIDEO_PROGRESS(");
|
||||
export const sendHeartbeat = findByCodeLazy(".QUESTS_HEARTBEAT(");
|
||||
export const questPath = "/quest-home";
|
||||
export const videoQuestLeeway = 24;
|
||||
export const leftClick = 0;
|
||||
export const middleClick = 1;
|
||||
export const rightClick = 2;
|
||||
|
||||
type HeartbeatDispatchResult =
|
||||
| { type: "success"; userStatus: Quest["userStatus"]; }
|
||||
| { type: "failure"; error: unknown; }
|
||||
| { type: "timeout"; };
|
||||
|
||||
export function setIgnoredQuestIDs(questIDs: string[], userId?: string): void {
|
||||
const currentUserID = userId ?? UserStore.getCurrentUser()?.id;
|
||||
if (!currentUserID) return;
|
||||
|
|
@ -76,8 +83,9 @@ export function getQuestProgress(quest: Quest, task: QuestTask) {
|
|||
|
||||
export function getQuestTarget(task: QuestTask) {
|
||||
const isWatch = task.type === QuestTaskType.WATCH_VIDEO || task.type === QuestTaskType.WATCH_VIDEO_ON_MOBILE;
|
||||
const { completeVideoQuestsQuicker } = settings.store;
|
||||
const raw = task.target;
|
||||
const adjusted = Math.max(0, raw - (isWatch ? videoQuestLeeway : 0));
|
||||
const adjusted = Math.max(0, raw - (isWatch && completeVideoQuestsQuicker ? videoQuestLeeway : 0));
|
||||
return { raw, adjusted };
|
||||
}
|
||||
|
||||
|
|
@ -262,22 +270,9 @@ export async function reportVideoQuestProgress(quest: Quest, currentProgress: nu
|
|||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const response = await RestAPI.post({
|
||||
url: `/quests/${quest.id}/video-progress`,
|
||||
body: { timestamp: currentProgress }
|
||||
});
|
||||
await reportProgress(quest.id, currentProgress);
|
||||
|
||||
if (!response || !response.body) {
|
||||
logger?.warn(`[${getFormattedNow()}] No response body received while reporting video progress for Quest ${questName} on attempt ${attempt}/${maxAttempts}.`);
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
logger?.info(`[${getFormattedNow()}] Quest ${questName} progress reported: ${currentProgress} seconds.`);
|
||||
logger?.info(`[${getFormattedNow()}] Quest ${questName} progress reported: ${currentProgress.toFixed(6)} seconds.`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger?.error(`[${getFormattedNow()}] Failed to report progress for Quest ${questName} on attempt ${attempt}/${maxAttempts}:`, error);
|
||||
|
|
@ -292,31 +287,96 @@ export async function reportVideoQuestProgress(quest: Quest, currentProgress: nu
|
|||
return false;
|
||||
}
|
||||
|
||||
export async function reportPlayGameQuestProgress(quest: Quest, terminal: boolean, logger?: Logger, options?: { attempts?: number, delay?: number; }): Promise<{ progress: number | null; }> {
|
||||
function getPlayGameProgressValue(userStatus: Quest["userStatus"]): number | null {
|
||||
const progressMap = userStatus?.progress;
|
||||
|
||||
if (!progressMap) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const progressPlayType = progressMap.PLAY_ON_DESKTOP?.value
|
||||
?? progressMap.PLAY_ON_XBOX?.value
|
||||
?? progressMap.PLAY_ON_PLAYSTATION?.value
|
||||
?? progressMap.PLAY_ACTIVITY?.value;
|
||||
|
||||
return progressPlayType ?? null;
|
||||
}
|
||||
|
||||
function waitForHeartbeatDispatchResult(questId: string, timeoutMs: number): Promise<HeartbeatDispatchResult> {
|
||||
return new Promise(resolve => {
|
||||
const successEvent = "QUESTS_SEND_HEARTBEAT_SUCCESS";
|
||||
const failureEvent = "QUESTS_SEND_HEARTBEAT_FAILURE";
|
||||
const timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
resolve({ type: "timeout" });
|
||||
}, timeoutMs);
|
||||
|
||||
function cleanup() {
|
||||
clearTimeout(timeoutId);
|
||||
FluxDispatcher.unsubscribe(successEvent, onSuccess);
|
||||
FluxDispatcher.unsubscribe(failureEvent, onFailure);
|
||||
}
|
||||
|
||||
function onSuccess(data: { questId?: string; userStatus?: Quest["userStatus"]; }) {
|
||||
if (data.questId !== questId) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
resolve({ type: "success", userStatus: data.userStatus ?? null });
|
||||
}
|
||||
|
||||
function onFailure(data: { questId?: string; error?: unknown; }) {
|
||||
if (data.questId !== questId) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup();
|
||||
resolve({ type: "failure", error: data.error });
|
||||
}
|
||||
|
||||
FluxDispatcher.subscribe(successEvent, onSuccess);
|
||||
FluxDispatcher.subscribe(failureEvent, onFailure);
|
||||
});
|
||||
}
|
||||
|
||||
export async function reportPlayGameQuestProgress(quest: Quest, terminal: boolean, logger?: Logger, options?: { attempts?: number, delay?: number; timeout?: number; applicationId?: string; streamKey?: string; }): Promise<{ progress: number | null; completed: boolean; }> {
|
||||
const questName = normalizeQuestName(quest.config.messages.questName);
|
||||
|
||||
if (!quest.userStatus?.enrolledAt) {
|
||||
logger?.warn(`[${getFormattedNow()}] Cannot send heartbeat for unenrolled Quest ${questName}.`);
|
||||
return { progress: null };
|
||||
return { progress: null, completed: false };
|
||||
} else if (quest.userStatus?.completedAt) {
|
||||
return { progress: null };
|
||||
return { progress: null, completed: true };
|
||||
}
|
||||
|
||||
const maxAttempts = options?.attempts ?? 1;
|
||||
const delay = options?.delay ?? 2500;
|
||||
const timeout = options?.timeout ?? 10000;
|
||||
const streamKey = options?.streamKey ?? `call:${quest.id}:1`;
|
||||
const applicationId = options?.applicationId ?? quest.config.application.id;
|
||||
const questPlayType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY;
|
||||
|
||||
if (!questPlayType) {
|
||||
logger?.warn(`[${getFormattedNow()}] Could not recognize the Quest type for Quest ${questName}.`);
|
||||
return { progress: null, completed: false };
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const response = await RestAPI.post({
|
||||
url: `/quests/${quest.id}/heartbeat`,
|
||||
body: {
|
||||
stream_key: `call:${quest.id}:1`,
|
||||
terminal
|
||||
}
|
||||
const dispatchResultPromise = waitForHeartbeatDispatchResult(quest.id, timeout);
|
||||
|
||||
await sendHeartbeat({
|
||||
questId: quest.id,
|
||||
streamKey,
|
||||
applicationId,
|
||||
terminal,
|
||||
});
|
||||
|
||||
if (!response || !response.body) {
|
||||
logger?.warn(`[${getFormattedNow()}] No response body received while sending heartbeat for Quest ${questName} on attempt ${attempt}/${maxAttempts}.`);
|
||||
const dispatchResult = await dispatchResultPromise;
|
||||
|
||||
if (dispatchResult.type === "failure") {
|
||||
logger?.error(`[${getFormattedNow()}] Failed to send heartbeat for Quest ${questName} on attempt ${attempt}/${maxAttempts}:`, dispatchResult.error);
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
|
@ -325,18 +385,22 @@ export async function reportPlayGameQuestProgress(quest: Quest, terminal: boolea
|
|||
continue;
|
||||
}
|
||||
|
||||
const { body } = snakeToCamel(response);
|
||||
const progressPlayType = body.progress.PLAY_ON_DESKTOP || body.progress.PLAY_ON_XBOX || body.progress.PLAY_ON_PLAYSTATION || body.progress.PLAY_ACTIVITY;
|
||||
const questPlayType = quest.config.taskConfigV2?.tasks.PLAY_ON_DESKTOP || quest.config.taskConfigV2?.tasks.PLAY_ON_XBOX || quest.config.taskConfigV2?.tasks.PLAY_ON_PLAYSTATION || quest.config.taskConfigV2?.tasks.PLAY_ACTIVITY;
|
||||
const progress = progressPlayType?.value || 1;
|
||||
if (dispatchResult.type === "timeout") {
|
||||
logger?.warn(`[${getFormattedNow()}] Timed out waiting for heartbeat dispatch for Quest ${questName} on attempt ${attempt}/${maxAttempts}.`);
|
||||
|
||||
if (!questPlayType) {
|
||||
logger?.warn(`[${getFormattedNow()}] Could not recognize the Quest type for Quest ${questName}.`);
|
||||
return { progress: null };
|
||||
if (attempt < maxAttempts) {
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const updatedQuest = refreshQuest(quest);
|
||||
const progress = getPlayGameProgressValue(dispatchResult.userStatus) ?? getQuestProgress(updatedQuest, questPlayType) ?? 0;
|
||||
const completed = !!dispatchResult.userStatus?.completedAt || !!updatedQuest.userStatus?.completedAt;
|
||||
|
||||
logger?.info(`[${getFormattedNow()}] Heartbeat sent for Quest ${questName} with progress: ${progress}/${questPlayType.target}.`);
|
||||
return { progress };
|
||||
return { progress, completed };
|
||||
} catch (error) {
|
||||
logger?.error(`[${getFormattedNow()}] Failed to send heartbeat for Quest ${questName} on attempt ${attempt}/${maxAttempts}:`, error);
|
||||
|
||||
|
|
@ -347,7 +411,7 @@ export async function reportPlayGameQuestProgress(quest: Quest, terminal: boolea
|
|||
}
|
||||
|
||||
logger?.warn(`[${getFormattedNow()}] Exhausted all ${maxAttempts} attempts to send heartbeat for Quest ${questName}.`);
|
||||
return { progress: null };
|
||||
return { progress: null, completed: false };
|
||||
}
|
||||
|
||||
export function getBadgeSize(value: string, negative: boolean): number {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue