feat: dynamic void fissure missions (#2214)
All checks were successful
Build Docker image / docker-arm64 (push) Successful in 59s
Build / build (push) Successful in 56s
Build Docker image / docker-amd64 (push) Successful in 1m6s

Closes #1512

Reviewed-on: #2214
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-06-20 04:47:45 -07:00 committed by Sainan
parent 3c64f17e34
commit 95136e6059
7 changed files with 276 additions and 203 deletions

View File

@ -1,6 +1,15 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getWorldState } from "@/src/services/worldStateService"; import { getWorldState, populateFissures } from "@/src/services/worldStateService";
import { version_compare } from "@/src/helpers/inventoryHelpers";
export const worldStateController: RequestHandler = (req, res) => { export const worldStateController: RequestHandler = async (req, res) => {
res.json(getWorldState(req.query.buildLabel as string | undefined)); const buildLabel = req.query.buildLabel as string | undefined;
const worldState = getWorldState(buildLabel);
// Omitting void fissures for versions prior to Dante Unbound to avoid script errors.
if (!buildLabel || version_compare(buildLabel, "2024.03.24.20.00") >= 0) {
await populateFissures(worldState);
}
res.json(worldState);
}; };

View File

@ -22,6 +22,7 @@ import { JSONStringify } from "json-with-bigint";
import { startWebServer } from "./services/webService"; import { startWebServer } from "./services/webService";
import { validateConfig } from "@/src/services/configWatcherService"; import { validateConfig } from "@/src/services/configWatcherService";
import { updateWorldStateCollections } from "./services/worldStateService";
// Patch JSON.stringify to work flawlessly with Bigints. // Patch JSON.stringify to work flawlessly with Bigints.
JSON.stringify = JSONStringify; JSON.stringify = JSONStringify;
@ -33,6 +34,11 @@ mongoose
.then(() => { .then(() => {
logger.info("Connected to MongoDB"); logger.info("Connected to MongoDB");
startWebServer(); startWebServer();
void updateWorldStateCollections();
setInterval(() => {
void updateWorldStateCollections();
}, 60_000);
}) })
.catch(error => { .catch(error => {
if (error instanceof Error) { if (error instanceof Error) {

View File

@ -0,0 +1,14 @@
import { IFissureDatabase } from "@/src/types/worldStateTypes";
import { model, Schema } from "mongoose";
const fissureSchema = new Schema<IFissureDatabase>({
Activation: Date,
Expiry: Date,
Node: String, // must be unique
Modifier: String,
Hard: Boolean
});
fissureSchema.index({ Expiry: 1 }, { expireAfterSeconds: 0 }); // With this, MongoDB will automatically delete expired entries.
export const Fissure = model<IFissureDatabase>("Fissure", fissureSchema);

View File

@ -1,12 +1,13 @@
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json"; import staticWorldState from "@/static/fixed_responses/worldState/worldState.json";
import fissureMissions from "@/static/fixed_responses/worldState/fissureMissions.json";
import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json"; import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json";
import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json";
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json";
import { buildConfig } from "@/src/services/buildConfigService"; import { buildConfig } from "@/src/services/buildConfigService";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { SRng } from "@/src/services/rngService"; import { getRandomElement, getRandomInt, SRng } from "@/src/services/rngService";
import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; import { eMissionType, ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus";
import { import {
ICalendarDay, ICalendarDay,
ICalendarEvent, ICalendarEvent,
@ -21,8 +22,9 @@ import {
IWorldState, IWorldState,
TCircuitGameMode TCircuitGameMode
} from "../types/worldStateTypes"; } from "../types/worldStateTypes";
import { version_compare } from "../helpers/inventoryHelpers"; import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { Fissure } from "../models/worldStateModel";
const sortieBosses = [ const sortieBosses = [
"SORTIE_BOSS_HYENA", "SORTIE_BOSS_HYENA",
@ -1110,6 +1112,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
Alerts: [], Alerts: [],
Sorties: [], Sorties: [],
LiteSorties: [], LiteSorties: [],
ActiveMissions: [],
GlobalUpgrades: [], GlobalUpgrades: [],
VoidStorms: [], VoidStorms: [],
EndlessXpChoices: [], EndlessXpChoices: [],
@ -1118,14 +1121,10 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
SyndicateMissions: [...staticWorldState.SyndicateMissions] SyndicateMissions: [...staticWorldState.SyndicateMissions]
}; };
// Omit void fissures for versions prior to Dante Unbound to avoid script errors.
if (buildLabel && version_compare(buildLabel, "2024.03.24.20.00") < 0) {
worldState.ActiveMissions = [];
if (version_compare(buildLabel, "2017.10.12.17.04") < 0) {
// Old versions seem to really get hung up on not being able to load these. // Old versions seem to really get hung up on not being able to load these.
if (buildLabel && version_compare(buildLabel, "2017.10.12.17.04") < 0) {
worldState.PVPChallengeInstances = []; worldState.PVPChallengeInstances = [];
} }
}
if (config.worldState?.starDays) { if (config.worldState?.starDays) {
worldState.Goals.push({ worldState.Goals.push({
@ -1364,6 +1363,24 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
return worldState; return worldState;
}; };
export const populateFissures = async (worldState: IWorldState): Promise<void> => {
const fissures = await Fissure.find({});
for (const fissure of fissures) {
const meta = ExportRegions[fissure.Node];
worldState.ActiveMissions.push({
_id: toOid(fissure._id),
Region: meta.systemIndex + 1,
Seed: 1337,
Activation: toMongoDate(fissure.Activation),
Expiry: toMongoDate(fissure.Expiry),
Node: fissure.Node,
MissionType: eMissionType[meta.missionIndex].tag,
Modifier: fissure.Modifier,
Hard: fissure.Hard
});
}
};
export const idToBountyCycle = (id: string): number => { export const idToBountyCycle = (id: string): number => {
return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000); return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000);
}; };
@ -1491,3 +1508,57 @@ const nightwaveTagToSeason: Record<string, number> = {
RadioLegionIntermissionSyndicate: 1, // Intermission I RadioLegionIntermissionSyndicate: 1, // Intermission I
RadioLegionSyndicate: 0 // The Wolf of Saturn Six RadioLegionSyndicate: 0 // The Wolf of Saturn Six
}; };
export const updateWorldStateCollections = async (): Promise<void> => {
const fissures = await Fissure.find();
const activeNodes = new Set<string>();
const tierToFurthestExpiry: Record<string, number> = {
VoidT1: 0,
VoidT2: 0,
VoidT3: 0,
VoidT4: 0,
VoidT5: 0,
VoidT6: 0,
VoidT1Hard: 0,
VoidT2Hard: 0,
VoidT3Hard: 0,
VoidT4Hard: 0,
VoidT5Hard: 0,
VoidT6Hard: 0
};
for (const fissure of fissures) {
activeNodes.add(fissure.Node);
const key = fissure.Modifier + (fissure.Hard ? "Hard" : "");
tierToFurthestExpiry[key] = Math.max(tierToFurthestExpiry[key], fissure.Expiry.getTime());
}
const deadline = Date.now() - 6 * unixTimesInMs.minute;
for (const [tier, expiry] of Object.entries(tierToFurthestExpiry)) {
if (expiry < deadline) {
const numFissures = getRandomInt(1, 3);
for (let i = 0; i != numFissures; ++i) {
const modifier = tier.replace("Hard", "") as
| "VoidT1"
| "VoidT2"
| "VoidT3"
| "VoidT4"
| "VoidT5"
| "VoidT6";
let node: string;
do {
node = getRandomElement(fissureMissions[modifier])!;
} while (activeNodes.has(node));
activeNodes.add(node);
await Fissure.insertOne({
Activation: new Date(),
Expiry: new Date(Date.now() + getRandomInt(60, 120) * unixTimesInMs.minute),
Node: node,
Modifier: modifier,
Hard: tier.indexOf("Hard") != -1 ? true : undefined
});
}
}
}
};

View File

@ -9,8 +9,8 @@ export interface IWorldState {
Sorties: ISortie[]; Sorties: ISortie[];
LiteSorties: ILiteSortie[]; LiteSorties: ILiteSortie[];
SyndicateMissions: ISyndicateMissionInfo[]; SyndicateMissions: ISyndicateMissionInfo[];
GlobalUpgrades: IGlobalUpgrade[];
ActiveMissions: IFissure[]; ActiveMissions: IFissure[];
GlobalUpgrades: IGlobalUpgrade[];
NodeOverrides: INodeOverride[]; NodeOverrides: INodeOverride[];
VoidStorms: IVoidStorm[]; VoidStorms: IVoidStorm[];
PVPChallengeInstances: IPVPChallengeInstance[]; PVPChallengeInstances: IPVPChallengeInstance[];
@ -86,6 +86,14 @@ export interface IFissure {
Hard?: boolean; Hard?: boolean;
} }
export interface IFissureDatabase {
Activation: Date;
Expiry: Date;
Node: string;
Modifier: "VoidT1" | "VoidT2" | "VoidT3" | "VoidT4" | "VoidT5" | "VoidT6";
Hard?: boolean;
}
export interface INodeOverride { export interface INodeOverride {
_id: IOid; _id: IOid;
Activation?: IMongoDate; Activation?: IMongoDate;

View File

@ -0,0 +1,154 @@
{
"VoidT1": [
"SolNode23",
"SolNode66",
"SolNode45",
"SolNode41",
"SolNode59",
"SolNode39",
"SolNode75",
"SolNode113",
"SolNode85",
"SolNode58",
"SolNode101",
"SolNode109",
"SolNode26",
"SolNode15",
"SolNode61",
"SolNode123",
"SolNode16",
"SolNode79",
"SolNode2",
"SolNode22",
"SolNode68",
"SolNode89",
"SolNode11",
"SolNode46",
"SolNode36",
"SolNode27",
"SolNode14",
"SolNode106",
"SolNode30",
"SolNode107",
"SolNode63",
"SolNode128"
],
"VoidT2": [
"SolNode141",
"SolNode149",
"SolNode10",
"SolNode93",
"SettlementNode11",
"SolNode137",
"SolNode132",
"SolNode73",
"SolNode82",
"SolNode25",
"SolNode88",
"SolNode126",
"SolNode135",
"SolNode74",
"SettlementNode15",
"SolNode147",
"SolNode67",
"SolNode20",
"SolNode42",
"SolNode18",
"SolNode31",
"SolNode139",
"SettlementNode12",
"SolNode100",
"SolNode140",
"SolNode70",
"SettlementNode1",
"SettlementNode14",
"SolNode50",
"SettlementNode2",
"SolNode146",
"SettlementNode3",
"SolNode97",
"SolNode125",
"SolNode19",
"SolNode121",
"SolNode96",
"SolNode131"
],
"VoidT3": [
"SolNode62",
"SolNode17",
"SolNode403",
"SolNode6",
"SolNode118",
"SolNode211",
"SolNode217",
"SolNode401",
"SolNode64",
"SolNode405",
"SolNode84",
"SolNode402",
"SolNode408",
"SolNode122",
"SolNode57",
"SolNode216",
"SolNode205",
"SolNode215",
"SolNode404",
"SolNode209",
"SolNode406",
"SolNode204",
"SolNode203",
"SolNode409",
"SolNode400",
"SolNode212",
"SolNode1",
"SolNode412",
"SolNode49",
"SolNode78",
"SolNode410",
"SolNode407",
"SolNode220"
],
"VoidT4": [
"SolNode188",
"SolNode403",
"SolNode189",
"SolNode21",
"SolNode102",
"SolNode171",
"SolNode196",
"SolNode184",
"SolNode185",
"SolNode76",
"SolNode195",
"SolNode164",
"SolNode401",
"SolNode405",
"SolNode56",
"SolNode402",
"SolNode408",
"SolNode4",
"SolNode181",
"SolNode406",
"SolNode162",
"SolNode72",
"SolNode407",
"SolNode177",
"SolNode404",
"SolNode400",
"SolNode409",
"SolNode43",
"SolNode166",
"SolNode172",
"SolNode412",
"SolNode187",
"SolNode38",
"SolNode175",
"SolNode81",
"SolNode48",
"SolNode410",
"SolNode153",
"SolNode173"
],
"VoidT5": ["SolNode747", "SolNode743", "SolNode742", "SolNode744", "SolNode745", "SolNode748", "SolNode746", "SolNode741"],
"VoidT6": ["SolNode717", "SolNode309", "SolNode718", "SolNode232", "SolNode230", "SolNode310"]
}

View File

@ -327,195 +327,6 @@
"Nodes": [] "Nodes": []
} }
], ],
"ActiveMissions": [
{
"_id": { "$oid": "663a7509d93367863785932d" },
"Region": 15,
"Seed": 80795,
"Activation": { "$date": { "$numberLong": "1715107081517" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode400",
"MissionType": "MT_EXTERMINATION",
"Modifier": "VoidT3",
"Hard": true
},
{
"_id": { "$oid": "663a75f959a5964cadb39879" },
"Region": 19,
"Seed": 32067,
"Activation": { "$date": { "$numberLong": "1715107321237" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode747",
"MissionType": "MT_INTEL",
"Modifier": "VoidT5",
"Hard": true
},
{
"_id": { "$oid": "663a779d3e347839ff301814" },
"Region": 7,
"Seed": 51739,
"Activation": { "$date": { "$numberLong": "1715107741454" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode64",
"MissionType": "MT_TERRITORY",
"Modifier": "VoidT3"
},
{
"_id": { "$oid": "663a77d916c199f4644ee67d" },
"Region": 17,
"Seed": 61179,
"Activation": { "$date": { "$numberLong": "1715107801647" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode718",
"MissionType": "MT_ALCHEMY",
"Modifier": "VoidT6"
},
{
"_id": { "$oid": "663a78c98a609b49b8410726" },
"Region": 3,
"Seed": 9520,
"Activation": { "$date": { "$numberLong": "1715108041501" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode79",
"MissionType": "MT_INTEL",
"Modifier": "VoidT1",
"Hard": true
},
{
"_id": { "$oid": "663a7df15eeabaac79b0a061" },
"Region": 6,
"Seed": 48861,
"Activation": { "$date": { "$numberLong": "1715109361974" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode67",
"MissionType": "MT_INTEL",
"Modifier": "VoidT2",
"Hard": true
},
{
"_id": { "$oid": "663a7df25eeabaac79b0a062" },
"Region": 5,
"Seed": 13550,
"Activation": { "$date": { "$numberLong": "1715109361974" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode10",
"MissionType": "MT_SABOTAGE",
"Modifier": "VoidT2",
"Hard": true
},
{
"_id": { "$oid": "663a83cdec0d5181435f1324" },
"Region": 19,
"Seed": 39392,
"Activation": { "$date": { "$numberLong": "1715110861506" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode742",
"MissionType": "MT_DEFENSE",
"Modifier": "VoidT5"
},
{
"_id": { "$oid": "663a83cdec0d5181435f1325" },
"Region": 19,
"Seed": 88668,
"Activation": { "$date": { "$numberLong": "1715110861506" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode743",
"MissionType": "MT_MOBILE_DEFENSE",
"Modifier": "VoidT5"
},
{
"_id": { "$oid": "663a83cdec0d5181435f1326" },
"Region": 19,
"Seed": 73823,
"Activation": { "$date": { "$numberLong": "1715110861506" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode741",
"MissionType": "MT_ASSAULT",
"Modifier": "VoidT5"
},
{
"_id": { "$oid": "663a878d23d1514873170466" },
"Region": 9,
"Seed": 88696,
"Activation": { "$date": { "$numberLong": "1715111821951" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode4",
"MissionType": "MT_EXTERMINATION",
"Modifier": "VoidT4",
"Hard": true
},
{
"_id": { "$oid": "663a887d4903098c10992fe6" },
"Region": 6,
"Seed": 66337,
"Activation": { "$date": { "$numberLong": "1715112061729" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode18",
"MissionType": "MT_TERRITORY",
"Modifier": "VoidT2"
},
{
"_id": { "$oid": "663a887d4903098c10992fe7" },
"Region": 10,
"Seed": 5135,
"Activation": { "$date": { "$numberLong": "1715112061729" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode149",
"MissionType": "MT_DEFENSE",
"Modifier": "VoidT2"
},
{
"_id": { "$oid": "663a8931586c301b1fbe63d3" },
"Region": 15,
"Seed": 32180,
"Activation": { "$date": { "$numberLong": "1715112241196" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode408",
"MissionType": "MT_DEFENSE",
"Modifier": "VoidT4"
},
{
"_id": { "$oid": "663a8931586c301b1fbe63d4" },
"Region": 12,
"Seed": 22521,
"Activation": { "$date": { "$numberLong": "1715112241196" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode181",
"MissionType": "MT_EXTERMINATION",
"Modifier": "VoidT4"
},
{
"_id": { "$oid": "663a8931586c301b1fbe63d5" },
"Region": 2,
"Seed": 28500,
"Activation": { "$date": { "$numberLong": "1715112241196" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode128",
"MissionType": "MT_EXTERMINATION",
"Modifier": "VoidT1"
},
{
"_id": { "$oid": "663a8931586c301b1fbe63d6" },
"Region": 3,
"Seed": 24747,
"Activation": { "$date": { "$numberLong": "1715112241196" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode26",
"MissionType": "MT_DEFENSE",
"Modifier": "VoidT1"
},
{
"_id": { "$oid": "663a8931586c301b1fbe63d7" },
"Region": 17,
"Seed": 63914,
"Activation": { "$date": { "$numberLong": "1715112241196" } },
"Expiry": { "$date": { "$numberLong": "2000000000000" } },
"Node": "SolNode717",
"MissionType": "MT_SURVIVAL",
"Modifier": "VoidT6",
"Hard": true
}
],
"NodeOverrides": [ "NodeOverrides": [
{ "_id": { "$oid": "549b18e9b029cef5991d6aec" }, "Node": "EuropaHUB", "Hide": true }, { "_id": { "$oid": "549b18e9b029cef5991d6aec" }, "Node": "EuropaHUB", "Hide": true },
{ "_id": { "$oid": "54a1737aeb658f6cbccf70ff" }, "Node": "ErisHUB", "Hide": true }, { "_id": { "$oid": "54a1737aeb658f6cbccf70ff" }, "Node": "ErisHUB", "Hide": true },