From 95136e6059b0d456d6e1174d6b2527c6944b70a6 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 20 Jun 2025 04:47:45 -0700 Subject: [PATCH] feat: dynamic void fissure missions (#2214) Closes #1512 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2214 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- .../dynamic/worldStateController.ts | 15 +- src/index.ts | 6 + src/models/worldStateModel.ts | 14 ++ src/services/worldStateService.ts | 91 ++++++++- src/types/worldStateTypes.ts | 10 +- .../worldState/fissureMissions.json | 154 ++++++++++++++ .../worldState/worldState.json | 189 ------------------ 7 files changed, 276 insertions(+), 203 deletions(-) create mode 100644 src/models/worldStateModel.ts create mode 100644 static/fixed_responses/worldState/fissureMissions.json diff --git a/src/controllers/dynamic/worldStateController.ts b/src/controllers/dynamic/worldStateController.ts index 89335497..75d78ff0 100644 --- a/src/controllers/dynamic/worldStateController.ts +++ b/src/controllers/dynamic/worldStateController.ts @@ -1,6 +1,15 @@ 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) => { - res.json(getWorldState(req.query.buildLabel as string | undefined)); +export const worldStateController: RequestHandler = async (req, res) => { + 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); }; diff --git a/src/index.ts b/src/index.ts index de36b392..7afd9387 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,6 +22,7 @@ import { JSONStringify } from "json-with-bigint"; import { startWebServer } from "./services/webService"; import { validateConfig } from "@/src/services/configWatcherService"; +import { updateWorldStateCollections } from "./services/worldStateService"; // Patch JSON.stringify to work flawlessly with Bigints. JSON.stringify = JSONStringify; @@ -33,6 +34,11 @@ mongoose .then(() => { logger.info("Connected to MongoDB"); startWebServer(); + + void updateWorldStateCollections(); + setInterval(() => { + void updateWorldStateCollections(); + }, 60_000); }) .catch(error => { if (error instanceof Error) { diff --git a/src/models/worldStateModel.ts b/src/models/worldStateModel.ts new file mode 100644 index 00000000..37615d7e --- /dev/null +++ b/src/models/worldStateModel.ts @@ -0,0 +1,14 @@ +import { IFissureDatabase } from "@/src/types/worldStateTypes"; +import { model, Schema } from "mongoose"; + +const fissureSchema = new Schema({ + 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("Fissure", fissureSchema); diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 8fa82eed..ea48ef44 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -1,12 +1,13 @@ 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 sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; import { buildConfig } from "@/src/services/buildConfigService"; import { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; -import { SRng } from "@/src/services/rngService"; -import { ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; +import { getRandomElement, getRandomInt, SRng } from "@/src/services/rngService"; +import { eMissionType, ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; import { ICalendarDay, ICalendarEvent, @@ -21,8 +22,9 @@ import { IWorldState, TCircuitGameMode } from "../types/worldStateTypes"; -import { version_compare } from "../helpers/inventoryHelpers"; +import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers"; import { logger } from "../utils/logger"; +import { Fissure } from "../models/worldStateModel"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -1110,6 +1112,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { Alerts: [], Sorties: [], LiteSorties: [], + ActiveMissions: [], GlobalUpgrades: [], VoidStorms: [], EndlessXpChoices: [], @@ -1118,13 +1121,9 @@ export const getWorldState = (buildLabel?: string): IWorldState => { 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. - worldState.PVPChallengeInstances = []; - } + // 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 = []; } if (config.worldState?.starDays) { @@ -1364,6 +1363,24 @@ export const getWorldState = (buildLabel?: string): IWorldState => { return worldState; }; +export const populateFissures = async (worldState: IWorldState): Promise => { + 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 => { return Math.trunc((parseInt(id.substring(0, 8), 16) * 1000) / 9000_000); }; @@ -1491,3 +1508,57 @@ const nightwaveTagToSeason: Record = { RadioLegionIntermissionSyndicate: 1, // Intermission I RadioLegionSyndicate: 0 // The Wolf of Saturn Six }; + +export const updateWorldStateCollections = async (): Promise => { + const fissures = await Fissure.find(); + + const activeNodes = new Set(); + const tierToFurthestExpiry: Record = { + 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 + }); + } + } + } +}; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index bd8ab138..73aa9d78 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -9,8 +9,8 @@ export interface IWorldState { Sorties: ISortie[]; LiteSorties: ILiteSortie[]; SyndicateMissions: ISyndicateMissionInfo[]; - GlobalUpgrades: IGlobalUpgrade[]; ActiveMissions: IFissure[]; + GlobalUpgrades: IGlobalUpgrade[]; NodeOverrides: INodeOverride[]; VoidStorms: IVoidStorm[]; PVPChallengeInstances: IPVPChallengeInstance[]; @@ -86,6 +86,14 @@ export interface IFissure { Hard?: boolean; } +export interface IFissureDatabase { + Activation: Date; + Expiry: Date; + Node: string; + Modifier: "VoidT1" | "VoidT2" | "VoidT3" | "VoidT4" | "VoidT5" | "VoidT6"; + Hard?: boolean; +} + export interface INodeOverride { _id: IOid; Activation?: IMongoDate; diff --git a/static/fixed_responses/worldState/fissureMissions.json b/static/fixed_responses/worldState/fissureMissions.json new file mode 100644 index 00000000..9b5c85e3 --- /dev/null +++ b/static/fixed_responses/worldState/fissureMissions.json @@ -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"] +} diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 73d48ce1..f973d69a 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -327,195 +327,6 @@ "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": [ { "_id": { "$oid": "549b18e9b029cef5991d6aec" }, "Node": "EuropaHUB", "Hide": true }, { "_id": { "$oid": "54a1737aeb658f6cbccf70ff" }, "Node": "ErisHUB", "Hide": true },