feat: recover nightwave challenges
All checks were successful
Build / build (pull_request) Successful in 55s

Closes #1534
This commit is contained in:
AMelonInsideLemon 2025-08-07 02:32:45 +02:00
parent 4a2d863c9c
commit 869ee9e2a2
4 changed files with 124 additions and 42 deletions

View File

@ -0,0 +1,62 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "@/src/services/worldStateService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { ISeasonChallenge } from "@/src/types/worldStateTypes";
import { ExportChallenges } from "warframe-public-export-plus";
export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress");
const worldState = getWorldState(undefined);
if (worldState.SeasonInfo) {
const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag);
const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong);
const nightwaveSeason = worldState.SeasonInfo.Season;
const timeMs = worldState.Time * 1000;
const completedChallengesIds = new Set<string>();
inventory.SeasonChallengeHistory.forEach(challengeHistory => {
const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1;
if (nightwaveSeason == entryNightwaveSeason) {
const meta = Object.entries(ExportChallenges).find(
([key]) => key.split("/").pop() === challengeHistory.challenge
);
if (meta) {
const [, challengeMeta] = meta;
const challengeProgress = inventory.ChallengeProgress.find(
c => c.Name === challengeHistory.challenge
);
if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) {
completedChallengesIds.add(challengeHistory.id);
}
}
}
});
const PastWeeklyChallenges: ISeasonChallenge[] = [];
let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1;
while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) {
const tempActs: ISeasonChallenge[] = [];
pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason);
for (const act of tempActs) {
if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) {
if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) {
act.Permanent = true;
}
PastWeeklyChallenges.push(act);
}
}
week--;
}
res.json({ PastWeeklyChallenges: PastWeeklyChallenges });
}
};

View File

@ -66,6 +66,7 @@ import { getGuildLogController } from "@/src/controllers/api/getGuildLogControll
import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController"; import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController";
import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController"; import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController";
import { getProfileViewingDataPostController } from "@/src/controllers/dynamic/getProfileViewingDataController"; import { getProfileViewingDataPostController } from "@/src/controllers/dynamic/getProfileViewingDataController";
import { getPastWeeklyChallengesController } from "@/src/controllers/api/getPastWeeklyChallengesController";
import { getShipController } from "@/src/controllers/api/getShipController"; import { getShipController } from "@/src/controllers/api/getShipController";
import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController"; import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController";
import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController"; import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController";
@ -195,6 +196,7 @@ apiRouter.get("/getGuildLog.php", getGuildLogController);
apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController);
apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17 apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17
apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController);
apiRouter.get("/getPastWeeklyChallenges.php", getPastWeeklyChallengesController)
apiRouter.get("/getShip.php", getShipController); apiRouter.get("/getShip.php", getShipController);
apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8 apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8
apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/getVendorInfo.php", getVendorInfoController);

View File

@ -180,7 +180,7 @@ const microplanetEndlessJobs: readonly string[] = [
"/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty" "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
]; ];
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0 export const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => { const isBeforeNextExpectedWorldStateRefresh = (nowMs: number, thenMs: number): boolean => {
return nowMs + 300_000 > thenMs; return nowMs + 300_000 > thenMs;
@ -365,10 +365,10 @@ interface IRotatingSeasonChallengePools {
daily: string[]; daily: string[];
weekly: string[]; weekly: string[];
hardWeekly: string[]; hardWeekly: string[];
hasWeeklyPermanent: boolean; weeklyPermanent: string[];
} }
const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => { export const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallengePools => {
const syndicate = ExportSyndicates[syndicateTag]; const syndicate = ExportSyndicates[syndicateTag];
return { return {
daily: syndicate.dailyChallenges!, daily: syndicate.dailyChallenges!,
@ -380,7 +380,7 @@ const getSeasonChallengePools = (syndicateTag: string): IRotatingSeasonChallenge
hardWeekly: syndicate.weeklyChallenges!.filter(x => hardWeekly: syndicate.weeklyChallenges!.filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/") x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
), ),
hasWeeklyPermanent: syndicate.weeklyChallenges!.some(x => weeklyPermanent: syndicate.weeklyChallenges!.filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent") x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
) )
}; };
@ -401,6 +401,7 @@ const getSeasonDailyChallenge = (pools: IRotatingSeasonChallengePools, day: numb
const pushSeasonWeeklyChallenge = ( const pushSeasonWeeklyChallenge = (
activeChallenges: ISeasonChallenge[], activeChallenges: ISeasonChallenge[],
pool: string[], pool: string[],
nightwaveSeason: number,
week: number, week: number,
id: number id: number
): void => { ): void => {
@ -413,51 +414,59 @@ const pushSeasonWeeklyChallenge = (
challenge = rng.randomElement(pool)!; challenge = rng.randomElement(pool)!;
} while (activeChallenges.some(x => x.Challenge == challenge)); } while (activeChallenges.some(x => x.Challenge == challenge));
activeChallenges.push({ activeChallenges.push({
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") }, _id: {
$oid:
(nightwaveSeason + 1).toString().padStart(4, "0") +
"bb2d9d00cb47" +
challengeId.toString().padStart(8, "0")
},
Activation: { $date: { $numberLong: weekStart.toString() } }, Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } }, Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: challenge Challenge: challenge
}); });
}; };
const pushWeeklyActs = ( export const pushWeeklyActs = (
activeChallenges: ISeasonChallenge[], activeChallenges: ISeasonChallenge[],
pools: IRotatingSeasonChallengePools, pools: IRotatingSeasonChallengePools,
week: number week: number,
nightwaveStartTimestamp: number,
nightwaveSeason: number
): void => { ): void => {
const weekStart = EPOCH + week * 604800000; pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 0);
const weekEnd = weekStart + 604800000; pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 1);
if (pools.weeklyPermanent.length > 0) {
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 0); const weekStart = EPOCH + week * unixTimesInMs.week;
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 1); const weekEnd = weekStart + unixTimesInMs.week;
if (pools.hasWeeklyPermanent) { const nightwaveWeekStart = ((): number => {
activeChallenges.push({ const nightwaveStartDate = new Date(nightwaveStartTimestamp);
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") }, const dayOffset = (nightwaveStartDate.getDay() + 6) % 7;
Activation: { $date: { $numberLong: weekStart.toString() } }, nightwaveStartDate.setDate(nightwaveStartDate.getDate() - dayOffset);
Expiry: { $date: { $numberLong: weekEnd.toString() } }, nightwaveStartDate.setHours(0, 0, 0, 0);
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" return nightwaveStartDate.getTime();
}); })();
activeChallenges.push({ const nightwaveWeek = Math.trunc((weekStart - nightwaveWeekStart) / unixTimesInMs.week);
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") }, const weeklyPermanentIndex = (nightwaveWeek * 3) % pools.weeklyPermanent.length;
Activation: { $date: { $numberLong: weekStart.toString() } }, for (let i = 0; i < 3; i++) {
Expiry: { $date: { $numberLong: weekEnd.toString() } }, activeChallenges.push({
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" _id: {
}); $oid:
activeChallenges.push({ (nightwaveSeason + 1).toString().padStart(4, "0") +
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") }, "b96e9d00cb47" +
Activation: { $date: { $numberLong: weekStart.toString() } }, (week * 7 + 2 + i).toString().padStart(8, "0")
Expiry: { $date: { $numberLong: weekEnd.toString() } }, },
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" Activation: { $date: { $numberLong: weekStart.toString() } },
}); Expiry: { $date: { $numberLong: weekEnd.toString() } },
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 2); Challenge: pools.weeklyPermanent[weeklyPermanentIndex + i]
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 3); });
}
} else { } else {
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 2); pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 2);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 3); pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 3);
pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, week, 4); pushSeasonWeeklyChallenge(activeChallenges, pools.weekly, nightwaveSeason, week, 4);
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 5);
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, week, 6);
} }
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 5);
pushSeasonWeeklyChallenge(activeChallenges, pools.hardWeekly, nightwaveSeason, week, 6);
}; };
const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => { const generateXpAmounts = (rng: SRng, stageCount: number, minXp: number, maxXp: number): number[] => {
@ -1552,11 +1561,13 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
// Nightwave Challenges // Nightwave Challenges
const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel); const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
if (nightwaveSyndicateTag) { if (nightwaveSyndicateTag) {
const nightwaveStartTimestamp = 1747851300000;
const nightwaveSeason = nightwaveTagToSeason[nightwaveSyndicateTag];
worldState.SeasonInfo = { worldState.SeasonInfo = {
Activation: { $date: { $numberLong: "1715796000000" } }, Activation: { $date: { $numberLong: nightwaveStartTimestamp.toString() } },
Expiry: { $date: { $numberLong: "2000000000000" } }, Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: nightwaveSyndicateTag, AffiliationTag: nightwaveSyndicateTag,
Season: nightwaveTagToSeason[nightwaveSyndicateTag], Season: nightwaveSeason,
Phase: 0, Phase: 0,
Params: "", Params: "",
ActiveChallenges: [] ActiveChallenges: []
@ -1568,9 +1579,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) { if (isBeforeNextExpectedWorldStateRefresh(timeMs, EPOCH + (day + 1) * 86400000)) {
worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1)); worldState.SeasonInfo.ActiveChallenges.push(getSeasonDailyChallenge(pools, day + 1));
} }
pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week); pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week, nightwaveStartTimestamp, nightwaveSeason);
if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
pushWeeklyActs(worldState.SeasonInfo.ActiveChallenges, pools, week + 1); pushWeeklyActs(
worldState.SeasonInfo.ActiveChallenges,
pools,
week + 1,
nightwaveStartTimestamp,
nightwaveSeason
);
} }
} }

View File

@ -265,6 +265,7 @@ export interface IEndlessXpChoice {
export interface ISeasonChallenge { export interface ISeasonChallenge {
_id: IOid; _id: IOid;
Daily?: boolean; Daily?: boolean;
Permanent?: boolean; // only for getPastWeeklyChallenges response
Activation: IMongoDate; Activation: IMongoDate;
Expiry: IMongoDate; Expiry: IMongoDate;
Challenge: string; Challenge: string;