Compare commits
	
		
			No commits in common. "main" and "main" have entirely different histories.
		
	
	
		
	
		
							
								
								
									
										25
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										25
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							@ -4,9 +4,9 @@ on:
 | 
			
		||||
        branches:
 | 
			
		||||
            - main
 | 
			
		||||
jobs:
 | 
			
		||||
    docker:
 | 
			
		||||
    docker-amd64:
 | 
			
		||||
        if: github.repository == 'OpenWF/SpaceNinjaServer'
 | 
			
		||||
        runs-on: ubuntu-latest
 | 
			
		||||
        runs-on: amd64
 | 
			
		||||
        steps:
 | 
			
		||||
            - name: Set up Docker buildx
 | 
			
		||||
              uses: docker/setup-buildx-action@v3
 | 
			
		||||
@ -18,10 +18,27 @@ jobs:
 | 
			
		||||
            - name: Build and push
 | 
			
		||||
              uses: docker/build-push-action@v6
 | 
			
		||||
              with:
 | 
			
		||||
                  platforms: linux/arm64,linux/amd64
 | 
			
		||||
                  platforms: linux/amd64
 | 
			
		||||
                  push: true
 | 
			
		||||
                  tags: |
 | 
			
		||||
                      openwf/spaceninjaserver:latest
 | 
			
		||||
                      openwf/spaceninjaserver:latest-arm64
 | 
			
		||||
                      openwf/spaceninjaserver:${{ github.sha }}
 | 
			
		||||
    docker-arm64:
 | 
			
		||||
        if: github.repository == 'OpenWF/SpaceNinjaServer'
 | 
			
		||||
        runs-on: arm64
 | 
			
		||||
        steps:
 | 
			
		||||
            - name: Set up Docker buildx
 | 
			
		||||
              uses: docker/setup-buildx-action@v3
 | 
			
		||||
            - name: Log in to container registry
 | 
			
		||||
              uses: docker/login-action@v3
 | 
			
		||||
              with:
 | 
			
		||||
                  username: openwf
 | 
			
		||||
                  password: ${{ secrets.DOCKERHUB_TOKEN }}
 | 
			
		||||
            - name: Build and push
 | 
			
		||||
              uses: docker/build-push-action@v6
 | 
			
		||||
              with:
 | 
			
		||||
                  platforms: linux/arm64
 | 
			
		||||
                  push: true
 | 
			
		||||
                  tags: |
 | 
			
		||||
                      openwf/spaceninjaserver:latest-arm64
 | 
			
		||||
                      openwf/spaceninjaserver:${{ github.sha }}-arm64
 | 
			
		||||
 | 
			
		||||
@ -14,13 +14,11 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
 | 
			
		||||
 | 
			
		||||
- `skipTutorial` affects only newly created accounts, so you may wish to change it before logging in for the first time.
 | 
			
		||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
			
		||||
- `ircExecutable` can be provided with a relative path to an EXE which will be ran as a child process of SpaceNinjaServer.
 | 
			
		||||
- `ircAddress`, `hubAddress`, and `nrsAddress` can be provided if these secondary servers are on a different machine.
 | 
			
		||||
- `ircAddress`, `hubAddress`, and `nrsAddress` are not present by default but can be provided if these secondary servers are on a different machine.
 | 
			
		||||
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
 | 
			
		||||
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
 | 
			
		||||
- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
 | 
			
		||||
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
 | 
			
		||||
  - `RadioLegionIntermission14Syndicate` for Nora's Mix: Dreams of the Dead
 | 
			
		||||
  - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
 | 
			
		||||
  - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
 | 
			
		||||
  - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
 | 
			
		||||
 | 
			
		||||
@ -8,10 +8,6 @@
 | 
			
		||||
  "bindAddress": "0.0.0.0",
 | 
			
		||||
  "httpPort": 80,
 | 
			
		||||
  "httpsPort": 443,
 | 
			
		||||
  "ircExecutable": null,
 | 
			
		||||
  "ircAddress": null,
 | 
			
		||||
  "hubAddress": null,
 | 
			
		||||
  "nrsAddress": null,
 | 
			
		||||
  "administratorNames": [],
 | 
			
		||||
  "autoCreateAccount": true,
 | 
			
		||||
  "skipTutorial": false,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
services:
 | 
			
		||||
    spaceninjaserver:
 | 
			
		||||
        # The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'.
 | 
			
		||||
        image: openwf/spaceninjaserver:latest
 | 
			
		||||
 | 
			
		||||
        volumes:
 | 
			
		||||
@ -18,6 +19,9 @@ services:
 | 
			
		||||
            - mongodb
 | 
			
		||||
    mongodb:
 | 
			
		||||
        image: docker.io/library/mongo:8.0.0-noble
 | 
			
		||||
        environment:
 | 
			
		||||
            MONGO_INITDB_ROOT_USERNAME: openwfagent
 | 
			
		||||
            MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
 | 
			
		||||
        volumes:
 | 
			
		||||
            - ./docker-data/database:/data/db
 | 
			
		||||
        command: mongod --quiet --logpath /dev/null
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
if [ ! -f conf/config.json ]; then
 | 
			
		||||
	jq --arg value "mongodb://mongodb:27017/openWF" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
 | 
			
		||||
	jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
exec npm run raw -- --configPath conf/config.json
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -18,7 +18,7 @@
 | 
			
		||||
        "morgan": "^1.10.0",
 | 
			
		||||
        "ncp": "^2.0.0",
 | 
			
		||||
        "undici": "^7.10.0",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.93",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.92",
 | 
			
		||||
        "warframe-riven-info": "^0.1.2",
 | 
			
		||||
        "winston": "^3.17.0",
 | 
			
		||||
        "winston-daily-rotate-file": "^5.0.0",
 | 
			
		||||
@ -5534,9 +5534,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-public-export-plus": {
 | 
			
		||||
      "version": "0.5.93",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.93.tgz",
 | 
			
		||||
      "integrity": "sha512-A8LSFJoyg7sU1n4L0zhLK1g0CREh8Fxvk7eXKoT8nMTroQg6YgEw02gK0MUi9U3rWTnlaGTsXZMp/tgC7HWUKw=="
 | 
			
		||||
      "version": "0.5.92",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.92.tgz",
 | 
			
		||||
      "integrity": "sha512-5O5VtyVXxKtl5QdpzoVyKov5GX6t3z/U5tqPq73kjoSyA5NQT2V9sWsZK4ASyY8Edv9hNsdwlZdsdP8QmYbubg=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-riven-info": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@
 | 
			
		||||
    "build:dev": "tsgo --inlineSourceMap",
 | 
			
		||||
    "build:dev:tsc": "tsc --incremental --inlineSourceMap",
 | 
			
		||||
    "build-and-start": "npm run build && npm run start",
 | 
			
		||||
    "build-and-start:bun": "npm run verify && npm run bun-run",
 | 
			
		||||
    "dev": "node scripts/dev.cjs",
 | 
			
		||||
    "dev:bun": "bun scripts/dev.cjs",
 | 
			
		||||
    "verify": "tsgo --noEmit",
 | 
			
		||||
@ -35,7 +36,7 @@
 | 
			
		||||
    "morgan": "^1.10.0",
 | 
			
		||||
    "ncp": "^2.0.0",
 | 
			
		||||
    "undici": "^7.10.0",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.93",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.92",
 | 
			
		||||
    "warframe-riven-info": "^0.1.2",
 | 
			
		||||
    "winston": "^3.17.0",
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0",
 | 
			
		||||
 | 
			
		||||
@ -40,10 +40,7 @@ function run(changedFile) {
 | 
			
		||||
        runproc = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const thisbuildproc = spawn(
 | 
			
		||||
        [process.versions.bun ? "bun" : "npm", "run", cangoraw ? "verify" : "build:dev"].join(" "),
 | 
			
		||||
        spawnopts
 | 
			
		||||
    );
 | 
			
		||||
    const thisbuildproc = spawn("npm", ["run", cangoraw ? "verify" : "build:dev"], spawnopts);
 | 
			
		||||
    const thisbuildstart = Date.now();
 | 
			
		||||
    buildproc = thisbuildproc;
 | 
			
		||||
    buildproc.on("exit", code => {
 | 
			
		||||
@ -54,13 +51,8 @@ function run(changedFile) {
 | 
			
		||||
        if (code === 0) {
 | 
			
		||||
            console.log(`${cangoraw ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
 | 
			
		||||
            runproc = spawn(
 | 
			
		||||
                [
 | 
			
		||||
                    process.versions.bun ? "bun" : "npm",
 | 
			
		||||
                    "run",
 | 
			
		||||
                    cangoraw ? (process.versions.bun ? "raw:bun" : "raw") : "start",
 | 
			
		||||
                    "--",
 | 
			
		||||
                    ...args
 | 
			
		||||
                ].join(" "),
 | 
			
		||||
                "npm",
 | 
			
		||||
                ["run", cangoraw ? (process.versions.bun ? "raw:bun" : "raw") : "start", "--", ...args],
 | 
			
		||||
                spawnopts
 | 
			
		||||
            );
 | 
			
		||||
            runproc.on("exit", () => {
 | 
			
		||||
 | 
			
		||||
@ -17,7 +17,7 @@ const app = express();
 | 
			
		||||
 | 
			
		||||
app.use((req, _res, next) => {
 | 
			
		||||
    // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
 | 
			
		||||
    // The client patch is expected to decrypt it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
 | 
			
		||||
    // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
 | 
			
		||||
    if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
 | 
			
		||||
        req.headers["content-encoding"] = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,3 @@
 | 
			
		||||
export const EPOCH = 1734307200_000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be the start of winter in 1999 iteration 0
 | 
			
		||||
 | 
			
		||||
const millisecondsPerSecond = 1000;
 | 
			
		||||
const secondsPerMinute = 60;
 | 
			
		||||
const minutesPerHour = 60;
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,9 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { deleteSession } from "../../managers/sessionManager.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { version_compare } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
 | 
			
		||||
const deleteSessionController: RequestHandler = async (_req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(_req);
 | 
			
		||||
const deleteSessionController: RequestHandler = (_req, res) => {
 | 
			
		||||
    deleteSession(_req.query.sessionId as string);
 | 
			
		||||
    if (account.BuildLabel && version_compare(account.BuildLabel, "2016.07.08.16.56") < 0) {
 | 
			
		||||
        // Pre-Specters of the Rail
 | 
			
		||||
        res.send(_req.query.sessionId as string); // Unsure if this is correct, but the client is chill with it
 | 
			
		||||
    } else {
 | 
			
		||||
        res.sendStatus(200);
 | 
			
		||||
    }
 | 
			
		||||
    res.sendStatus(200);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { deleteSessionController };
 | 
			
		||||
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const feedPrinceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "MiscItems NokkoColony NodeIntrosCompleted");
 | 
			
		||||
    const payload = getJSONfromString<IFeedPrinceRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    switch (payload.Mode) {
 | 
			
		||||
        case "r": {
 | 
			
		||||
            inventory.NokkoColony ??= {
 | 
			
		||||
                FeedLevel: 0,
 | 
			
		||||
                JournalEntries: []
 | 
			
		||||
            };
 | 
			
		||||
            const InventoryChanges: IInventoryChanges = {};
 | 
			
		||||
            inventory.NokkoColony.FeedLevel += payload.Amount;
 | 
			
		||||
            if (
 | 
			
		||||
                (!inventory.NodeIntrosCompleted.includes("CompletedVision1") && inventory.NokkoColony.FeedLevel > 20) ||
 | 
			
		||||
                (!inventory.NodeIntrosCompleted.includes("CompletedVision2") && inventory.NokkoColony.FeedLevel > 60)
 | 
			
		||||
            ) {
 | 
			
		||||
                res.json({
 | 
			
		||||
                    FeedSucceeded: false,
 | 
			
		||||
                    FeedLevel: inventory.NokkoColony.FeedLevel - payload.Amount,
 | 
			
		||||
                    InventoryChanges
 | 
			
		||||
                } satisfies IFeedPrinceResponse);
 | 
			
		||||
            } else {
 | 
			
		||||
                addMiscItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Types/Items/MiscItems/MushroomFood",
 | 
			
		||||
                    payload.Amount * -1,
 | 
			
		||||
                    InventoryChanges
 | 
			
		||||
                );
 | 
			
		||||
                await inventory.save();
 | 
			
		||||
                res.json({
 | 
			
		||||
                    FeedSucceeded: true,
 | 
			
		||||
                    FeedLevel: inventory.NokkoColony.FeedLevel,
 | 
			
		||||
                    InventoryChanges
 | 
			
		||||
                } satisfies IFeedPrinceResponse);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
            logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
            throw new Error(`unknown feedPrince mode: ${payload.Mode}`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IFeedPrinceRequest {
 | 
			
		||||
    Mode: string; // r
 | 
			
		||||
    Amount: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IFeedPrinceResponse {
 | 
			
		||||
    FeedSucceeded: boolean;
 | 
			
		||||
    FeedLevel: number;
 | 
			
		||||
    InventoryChanges: IInventoryChanges;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
 | 
			
		||||
import { EPOCH, unixTimesInMs } from "../../constants/timeConstants.ts";
 | 
			
		||||
import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
 | 
			
		||||
import { unixTimesInMs } from "../../constants/timeConstants.ts";
 | 
			
		||||
import type { ISeasonChallenge } from "../../types/worldStateTypes.ts";
 | 
			
		||||
import { ExportChallenges } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,12 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { parseString } from "../../helpers/general.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { giveKeyChainItem } from "../../services/questService.ts";
 | 
			
		||||
import type { IKeyChainRequest } from "../../types/requestTypes.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
 | 
			
		||||
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const accountId = parseString(req.query.accountId);
 | 
			
		||||
    const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString());
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
 | 
			
		||||
@ -13,8 +13,8 @@ const hostSessionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const session = createNewSession(hostSessionRequest, account._id);
 | 
			
		||||
    logger.debug(`New Session Created`, { session });
 | 
			
		||||
 | 
			
		||||
    if (account.BuildLabel && version_compare(account.BuildLabel, "2016.07.08.16.56") < 0) {
 | 
			
		||||
        // Pre-Specters of the Rail
 | 
			
		||||
    if (account.BuildLabel && version_compare(account.BuildLabel, "2015.03.21.08.17") < 0) {
 | 
			
		||||
        // U15 or below
 | 
			
		||||
        res.send(session.sessionId.toString());
 | 
			
		||||
    } else {
 | 
			
		||||
        res.json({ sessionId: toOid2(session.sessionId, account.BuildLabel), rewardSeed: 99999999 });
 | 
			
		||||
 | 
			
		||||
@ -310,13 +310,12 @@ export const getInventoryResponse = async (
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (inventory.skipAllDialogue) {
 | 
			
		||||
        inventoryResponse.TauntHistory ??= [];
 | 
			
		||||
        if (!inventoryResponse.TauntHistory.find(x => x.node == "TreasureTutorial")) {
 | 
			
		||||
            inventoryResponse.TauntHistory.push({
 | 
			
		||||
        inventoryResponse.TauntHistory = [
 | 
			
		||||
            {
 | 
			
		||||
                node: "TreasureTutorial",
 | 
			
		||||
                state: "TS_COMPLETED"
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        for (const str of allDialogue) {
 | 
			
		||||
            addString(inventoryResponse.NodeIntrosCompleted, str);
 | 
			
		||||
        }
 | 
			
		||||
@ -354,11 +353,11 @@ export const getInventoryResponse = async (
 | 
			
		||||
        if (!xpBasedLevelCapDisabled) {
 | 
			
		||||
            // This client has not been patched to accept any mastery rank, need to fake the XP.
 | 
			
		||||
            inventoryResponse.XPInfo = [];
 | 
			
		||||
            let numFrames = getExpRequiredForMr(Math.min(inventory.spoofMasteryRank, 5030)) / (30 * 200);
 | 
			
		||||
            let numFrames = getExpRequiredForMr(Math.min(inventory.spoofMasteryRank, 5030)) / 6000;
 | 
			
		||||
            while (numFrames-- > 0) {
 | 
			
		||||
                inventoryResponse.XPInfo.push({
 | 
			
		||||
                    ItemType: "/Lotus/Powersuits/Mag/Mag",
 | 
			
		||||
                    XP: 900_000 // Enough for rank 30 as per https://wiki.warframe.com/w/Affinity
 | 
			
		||||
                    XP: 1_600_000
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@ -109,19 +109,12 @@ const createLoginResponse = (
 | 
			
		||||
    const resp: ILoginResponse = {
 | 
			
		||||
        id: account.id,
 | 
			
		||||
        DisplayName: account.DisplayName,
 | 
			
		||||
        CountryCode: account.CountryCode,
 | 
			
		||||
        AmazonAuthToken: account.AmazonAuthToken,
 | 
			
		||||
        AmazonRefreshToken: account.AmazonRefreshToken,
 | 
			
		||||
        Nonce: account.Nonce,
 | 
			
		||||
        BuildLabel: buildLabel
 | 
			
		||||
    };
 | 
			
		||||
    if (version_compare(buildLabel, "2014.10.24.08.24") >= 0) {
 | 
			
		||||
        // U15 and up
 | 
			
		||||
        resp.CountryCode = account.CountryCode;
 | 
			
		||||
    } else {
 | 
			
		||||
        // U8
 | 
			
		||||
        resp.NatHash = "0";
 | 
			
		||||
        resp.SteamId = "0";
 | 
			
		||||
    }
 | 
			
		||||
    if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) {
 | 
			
		||||
        resp.NRS = [config.nrsAddress ?? myAddress];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,117 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { IJournalEntry } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IAffiliationMods } from "../../types/purchaseTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const researchMushroomController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "MiscItems NokkoColony Affiliations");
 | 
			
		||||
    const payload = getJSONfromString<IResearchMushroom>(String(req.body));
 | 
			
		||||
    switch (payload.Mode) {
 | 
			
		||||
        case "r": {
 | 
			
		||||
            const InventoryChanges = {};
 | 
			
		||||
            const AffiliationMods: IAffiliationMods[] = [];
 | 
			
		||||
 | 
			
		||||
            addMiscItem(inventory, payload.MushroomItem, payload.Amount * -1, InventoryChanges);
 | 
			
		||||
            if (payload.Convert) {
 | 
			
		||||
                addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/MushroomFood", payload.Amount, InventoryChanges);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            inventory.NokkoColony ??= {
 | 
			
		||||
                FeedLevel: 0,
 | 
			
		||||
                JournalEntries: []
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let journalEntry = inventory.NokkoColony.JournalEntries.find(x => x.EntryType == payload.MushroomItem);
 | 
			
		||||
            if (!journalEntry) {
 | 
			
		||||
                journalEntry = { EntryType: payload.MushroomItem, Progress: 0 };
 | 
			
		||||
                inventory.NokkoColony.JournalEntries.push(journalEntry);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let syndicate = inventory.Affiliations.find(x => x.Tag == "NightcapJournalSyndicate");
 | 
			
		||||
            if (!syndicate) {
 | 
			
		||||
                syndicate = { Tag: "NightcapJournalSyndicate", Title: 0, Standing: 0 };
 | 
			
		||||
                inventory.Affiliations.push(syndicate);
 | 
			
		||||
            }
 | 
			
		||||
            const completedBefore = inventory.NokkoColony.JournalEntries.filter(
 | 
			
		||||
                entry => getJournalRank(entry) === 3
 | 
			
		||||
            ).length;
 | 
			
		||||
            const PrevRank = syndicateTitleThresholds.reduce(
 | 
			
		||||
                (rank, threshold, i) => (completedBefore >= threshold ? i : rank),
 | 
			
		||||
                0
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (getJournalRank(journalEntry) < 3) journalEntry.Progress += payload.Amount;
 | 
			
		||||
 | 
			
		||||
            const completedAfter = inventory.NokkoColony.JournalEntries.filter(
 | 
			
		||||
                entry => getJournalRank(entry) === 3
 | 
			
		||||
            ).length;
 | 
			
		||||
            const NewRank = syndicateTitleThresholds.reduce(
 | 
			
		||||
                (rank, threshold, i) => (completedAfter >= threshold ? i : rank),
 | 
			
		||||
                0
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (NewRank > (syndicate.Title ?? 0)) {
 | 
			
		||||
                syndicate.Title = NewRank;
 | 
			
		||||
                AffiliationMods.push({ Tag: "NightcapJournalSyndicate", Title: NewRank });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                PrevRank,
 | 
			
		||||
                NewRank,
 | 
			
		||||
                Progress: journalEntry.Progress,
 | 
			
		||||
                InventoryChanges,
 | 
			
		||||
                AffiliationMods
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
            logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
            throw new Error(`unknown researchMushroom mode: ${payload.Mode}`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IResearchMushroom {
 | 
			
		||||
    Mode: string; // r
 | 
			
		||||
    MushroomItem: string;
 | 
			
		||||
    Amount: number;
 | 
			
		||||
    Convert: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const journalEntriesRank: Record<string, number> = {
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/PlainMushroomJournalItem": 1,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/GasMushroomJournalItem": 4,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/ToxinMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/ViralMushroomJournalItem": 4,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/MagneticMushroomJournalItem": 4,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/ElectricMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/TauMushroomJournalItem": 5,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/SlashMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/BlastMushroomJournalItem": 4,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/ImpactMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/ColdMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/CorrosiveMushroomJournalItem": 4,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/PunctureMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/HeatMushroomJournalItem": 3,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/RadiationMushroomJournalItem": 4,
 | 
			
		||||
    "/Lotus/Types/Items/MushroomJournal/VoidMushroomJournalItem": 5
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const syndicateTitleThresholds = [0, 1, 2, 6, 12, 16];
 | 
			
		||||
 | 
			
		||||
const getJournalRank = (journalEntry: IJournalEntry): number => {
 | 
			
		||||
    const k = journalEntriesRank[journalEntry.EntryType];
 | 
			
		||||
    if (!k) return 0;
 | 
			
		||||
 | 
			
		||||
    const thresholds = [k * 1, k * 3, k * 6];
 | 
			
		||||
 | 
			
		||||
    if (journalEntry.Progress >= thresholds[2]) return 3;
 | 
			
		||||
    if (journalEntry.Progress >= thresholds[1]) return 2;
 | 
			
		||||
    if (journalEntry.Progress >= thresholds[0]) return 1;
 | 
			
		||||
    return 0;
 | 
			
		||||
};
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { resetQuestKeyToStage } from "../../services/questService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { IKeyChainRequest } from "../../types/requestTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const reverseQuestProgressController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString());
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    resetQuestKeyToStage(inventory, keyChainInfo);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
@ -96,15 +96,10 @@ export const upgradesController: RequestHandler = async (req, res) => {
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker":
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker":
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/WeaponMeleeArcaneUnlocker":
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker":
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/WeaponArchGunArcaneUnlocker": {
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/WeaponAmpArcaneUnlocker": {
 | 
			
		||||
                    const item = inventory[payload.ItemCategory].id(payload.ItemId.$oid)!;
 | 
			
		||||
                    item.Features ??= 0;
 | 
			
		||||
                    if (operation.OperationType == "UOT_ARCANE_UNLOCK_1") {
 | 
			
		||||
                        item.Features |= EquipmentFeatures.SECOND_ARCANE_SLOT;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        item.Features |= EquipmentFeatures.ARCANE_SLOT;
 | 
			
		||||
                    }
 | 
			
		||||
                    item.Features |= EquipmentFeatures.ARCANE_SLOT;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                case "/Lotus/Types/Items/MiscItems/ValenceAdapter": {
 | 
			
		||||
 | 
			
		||||
@ -36,11 +36,6 @@ export const completeAllMissionsController: RequestHandler = async (req, res) =>
 | 
			
		||||
    }
 | 
			
		||||
    addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked");
 | 
			
		||||
    addString(inventory.NodeIntrosCompleted, "CetusSyndicate_IntroJob");
 | 
			
		||||
    let syndicate = inventory.Affiliations.find(x => x.Tag == "CetusSyndicate");
 | 
			
		||||
    if (!syndicate) {
 | 
			
		||||
        syndicate =
 | 
			
		||||
            inventory.Affiliations[inventory.Affiliations.push({ Tag: "CetusSyndicate", Standing: 250, Title: 0 })]; // Non-zero standing avoids Konzu's "prove yourself" text. 250 is identical to newbie bounty + bonus
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    switch (operation) {
 | 
			
		||||
        case "completeAll": {
 | 
			
		||||
            for (const questKey of inventory.QuestKeys) {
 | 
			
		||||
                await completeQuest(inventory, questKey.ItemType, undefined);
 | 
			
		||||
                await completeQuest(inventory, questKey.ItemType);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
@ -61,7 +61,6 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            inventory.QuestKeys.pull({ ItemType: questItemType });
 | 
			
		||||
            if (inventory.ActiveQuest == questItemType) inventory.ActiveQuest = "";
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "completeKey": {
 | 
			
		||||
@ -72,7 +71,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                await completeQuest(inventory, questItemType, undefined);
 | 
			
		||||
                await completeQuest(inventory, questItemType);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
@ -137,7 +136,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
                if (currentStage + 1 == questManifest.chainStages?.length) {
 | 
			
		||||
                    logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`);
 | 
			
		||||
                    await completeQuest(inventory, questKey.ItemType, undefined, true);
 | 
			
		||||
                    await completeQuest(inventory, questKey.ItemType, true);
 | 
			
		||||
                } else {
 | 
			
		||||
                    if (run > 0) {
 | 
			
		||||
                        questKey.Progress[currentStage + 1].c = run;
 | 
			
		||||
@ -151,14 +150,10 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    if (currentStage > 0) {
 | 
			
		||||
                        await giveKeyChainMissionReward(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            {
 | 
			
		||||
                                KeyChain: questKey.ItemType,
 | 
			
		||||
                                ChainStage: currentStage
 | 
			
		||||
                            },
 | 
			
		||||
                            undefined
 | 
			
		||||
                        );
 | 
			
		||||
                        await giveKeyChainMissionReward(inventory, {
 | 
			
		||||
                            KeyChain: questKey.ItemType,
 | 
			
		||||
                            ChainStage: currentStage
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										12
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								src/index.ts
									
									
									
									
									
								
							@ -19,7 +19,6 @@ logger.info("Starting up...");
 | 
			
		||||
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
 | 
			
		||||
import mongoose from "mongoose";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import child_process from "child_process";
 | 
			
		||||
import { JSONStringify } from "json-with-bigint";
 | 
			
		||||
import { startWebServer } from "./services/webService.ts";
 | 
			
		||||
import { validateConfig } from "./services/configWatcherService.ts";
 | 
			
		||||
@ -44,17 +43,6 @@ mongoose
 | 
			
		||||
 | 
			
		||||
        startWebServer();
 | 
			
		||||
 | 
			
		||||
        if (config.ircExecutable) {
 | 
			
		||||
            logger.info(`Starting IRC server: ${config.ircExecutable}`);
 | 
			
		||||
            child_process.execFile(config.ircExecutable, (error, _stdout, _stderr) => {
 | 
			
		||||
                if (error) {
 | 
			
		||||
                    logger.warn(`Failed to start IRC server`, error);
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.warn(`IRC server terminated unexpectedly`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        void updateWorldStateCollections();
 | 
			
		||||
        setInterval(() => {
 | 
			
		||||
            void updateWorldStateCollections();
 | 
			
		||||
 | 
			
		||||
@ -88,9 +88,7 @@ import type {
 | 
			
		||||
    IGoalProgressDatabase,
 | 
			
		||||
    IGoalProgressClient,
 | 
			
		||||
    IKubrowPetPrintClient,
 | 
			
		||||
    IKubrowPetPrintDatabase,
 | 
			
		||||
    INokkoColony,
 | 
			
		||||
    IJournalEntry
 | 
			
		||||
    IKubrowPetPrintDatabase
 | 
			
		||||
} from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IOid, ITypeCount } from "../../types/commonTypes.ts";
 | 
			
		||||
@ -1426,22 +1424,6 @@ const hubNpcCustomizationSchema = new Schema<IHubNpcCustomization>(
 | 
			
		||||
    { _id: false }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const journalEntrySchema = new Schema<IJournalEntry>(
 | 
			
		||||
    {
 | 
			
		||||
        EntryType: String,
 | 
			
		||||
        Progress: Number
 | 
			
		||||
    },
 | 
			
		||||
    { _id: false }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const nokkoColonySchema = new Schema<INokkoColony>(
 | 
			
		||||
    {
 | 
			
		||||
        FeedLevel: Number,
 | 
			
		||||
        JournalEntries: [journalEntrySchema]
 | 
			
		||||
    },
 | 
			
		||||
    { _id: false }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
			
		||||
    {
 | 
			
		||||
        accountOwnerId: Schema.Types.ObjectId,
 | 
			
		||||
@ -1858,9 +1840,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
			
		||||
 | 
			
		||||
        ClaimedJunctionChallengeRewards: { type: [String], default: undefined },
 | 
			
		||||
 | 
			
		||||
        SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined },
 | 
			
		||||
 | 
			
		||||
        NokkoColony: { type: nokkoColonySchema, default: undefined }
 | 
			
		||||
        SpecialItemRewardAttenuation: { type: [rewardAttenutationSchema], default: undefined }
 | 
			
		||||
    },
 | 
			
		||||
    { timestamps: { createdAt: "Created", updatedAt: false } }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ import { addPendingFriendController } from "../controllers/api/addPendingFriendC
 | 
			
		||||
import { addToAllianceController } from "../controllers/api/addToAllianceController.ts";
 | 
			
		||||
import { addToGuildController } from "../controllers/api/addToGuildController.ts";
 | 
			
		||||
import { adoptPetController } from "../controllers/api/adoptPetController.ts";
 | 
			
		||||
import { aggregateSessionsController } from "../controllers/dynamic/aggregateSessionsController.ts";
 | 
			
		||||
import { apartmentController } from "../controllers/api/apartmentController.ts";
 | 
			
		||||
import { arcaneCommonController } from "../controllers/api/arcaneCommonController.ts";
 | 
			
		||||
import { archonFusionController } from "../controllers/api/archonFusionController.ts";
 | 
			
		||||
@ -51,7 +50,6 @@ import { dronesController } from "../controllers/api/dronesController.ts";
 | 
			
		||||
import { endlessXpController } from "../controllers/api/endlessXpController.ts";
 | 
			
		||||
import { entratiLabConquestModeController } from "../controllers/api/entratiLabConquestModeController.ts";
 | 
			
		||||
import { evolveWeaponController } from "../controllers/api/evolveWeaponController.ts";
 | 
			
		||||
import { feedPrinceController } from "../controllers/api/feedPrinceController.ts";
 | 
			
		||||
import { findSessionsController } from "../controllers/api/findSessionsController.ts";
 | 
			
		||||
import { fishmongerController } from "../controllers/api/fishmongerController.ts";
 | 
			
		||||
import { focusController } from "../controllers/api/focusController.ts";
 | 
			
		||||
@ -118,10 +116,8 @@ import { removeFromGuildController } from "../controllers/api/removeFromGuildCon
 | 
			
		||||
import { removeIgnoredUserController } from "../controllers/api/removeIgnoredUserController.ts";
 | 
			
		||||
import { renamePetController } from "../controllers/api/renamePetController.ts";
 | 
			
		||||
import { rerollRandomModController } from "../controllers/api/rerollRandomModController.ts";
 | 
			
		||||
import { researchMushroomController } from "../controllers/api/researchMushroomController.ts";
 | 
			
		||||
import { resetQuestProgressController } from "../controllers/api/resetQuestProgressController.ts";
 | 
			
		||||
import { retrievePetFromStasisController } from "../controllers/api/retrievePetFromStasisController.ts";
 | 
			
		||||
import { reverseQuestProgressController } from "../controllers/api/reverseQuestProgressController.ts";
 | 
			
		||||
import { saveDialogueController } from "../controllers/api/saveDialogueController.ts";
 | 
			
		||||
import { saveLoadoutController } from "../controllers/api/saveLoadoutController.ts";
 | 
			
		||||
import { saveSettingsController } from "../controllers/api/saveSettingsController.ts";
 | 
			
		||||
@ -171,7 +167,6 @@ import { upgradeOperatorController } from "../controllers/api/upgradeOperatorCon
 | 
			
		||||
import { upgradesController } from "../controllers/api/upgradesController.ts";
 | 
			
		||||
import { valenceSwapController } from "../controllers/api/valenceSwapController.ts";
 | 
			
		||||
import { wishlistController } from "../controllers/api/wishlistController.ts";
 | 
			
		||||
import { worldStateController } from "../controllers/dynamic/worldStateController.ts";
 | 
			
		||||
 | 
			
		||||
const apiRouter = express.Router();
 | 
			
		||||
 | 
			
		||||
@ -237,7 +232,6 @@ apiRouter.get("/surveys.php", surveysController);
 | 
			
		||||
apiRouter.get("/trading.php", tradingController);
 | 
			
		||||
apiRouter.get("/updateSession.php", updateSessionGetController);
 | 
			
		||||
apiRouter.get("/upgradeOperator.php", upgradeOperatorController);
 | 
			
		||||
apiRouter.get("/worldState.php", worldStateController); // U8
 | 
			
		||||
 | 
			
		||||
// post
 | 
			
		||||
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
 | 
			
		||||
@ -249,7 +243,6 @@ apiRouter.post("/addPendingFriend.php", addPendingFriendController);
 | 
			
		||||
apiRouter.post("/addToAlliance.php", addToAllianceController);
 | 
			
		||||
apiRouter.post("/addToGuild.php", addToGuildController);
 | 
			
		||||
apiRouter.post("/adoptPet.php", adoptPetController);
 | 
			
		||||
apiRouter.post("/aggregateSessions.php", aggregateSessionsController); // Pre-Specters of the Rail builds
 | 
			
		||||
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
 | 
			
		||||
apiRouter.post("/archonFusion.php", archonFusionController);
 | 
			
		||||
apiRouter.post("/artifacts.php", artifactsController);
 | 
			
		||||
@ -278,7 +271,6 @@ apiRouter.post("/drones.php", dronesController);
 | 
			
		||||
apiRouter.post("/endlessXp.php", endlessXpController);
 | 
			
		||||
apiRouter.post("/entratiLabConquestMode.php", entratiLabConquestModeController);
 | 
			
		||||
apiRouter.post("/evolveWeapon.php", evolveWeaponController);
 | 
			
		||||
apiRouter.post("/feedPrince.php", feedPrinceController);
 | 
			
		||||
apiRouter.post("/findSessions.php", findSessionsController);
 | 
			
		||||
apiRouter.post("/fishmonger.php", fishmongerController);
 | 
			
		||||
apiRouter.post("/focus.php", focusController);
 | 
			
		||||
@ -326,9 +318,7 @@ apiRouter.post("/removeFromGuild.php", removeFromGuildController);
 | 
			
		||||
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
 | 
			
		||||
apiRouter.post("/renamePet.php", renamePetController);
 | 
			
		||||
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
 | 
			
		||||
apiRouter.post("/researchMushroom.php", researchMushroomController);
 | 
			
		||||
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
 | 
			
		||||
apiRouter.post("/reverseQuestProgress.php", reverseQuestProgressController);
 | 
			
		||||
apiRouter.post("/saveDialogue.php", saveDialogueController);
 | 
			
		||||
apiRouter.post("/saveLoadout.php", saveLoadoutController);
 | 
			
		||||
apiRouter.post("/saveSettings.php", saveSettingsController);
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import express from "express";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import fs from "fs/promises";
 | 
			
		||||
import { repoDir, rootDir } from "../helpers/pathHelper.ts";
 | 
			
		||||
import { args } from "../helpers/commandLineArguments.ts";
 | 
			
		||||
 | 
			
		||||
@ -22,62 +21,50 @@ webuiRouter.use("/webui", (req, res, next) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Serve virtual routes
 | 
			
		||||
webuiRouter.get("/webui/inventory", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/inventory", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/detailedView", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/detailedView", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/mods", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/mods", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/settings", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/settings", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/quests", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/quests", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/cheats", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/cheats", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/import", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/import", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/guildView", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, "static/webui/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/guildView", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, "static/webui/index.html"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Serve static files
 | 
			
		||||
webuiRouter.use("/webui", express.static(path.join(baseDir, "static/webui")));
 | 
			
		||||
 | 
			
		||||
// Serve favicon
 | 
			
		||||
webuiRouter.get("/favicon.ico", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "image/vnd.microsoft.icon");
 | 
			
		||||
    res.send(await fs.readFile(path.join(repoDir, "static/fixed_responses/favicon.ico")));
 | 
			
		||||
webuiRouter.get("/favicon.ico", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(repoDir, "static/fixed_responses/favicon.ico"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Serve warframe-riven-info
 | 
			
		||||
webuiRouter.get("/webui/riven-tool/", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/html;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(repoDir, "node_modules/warframe-riven-info/index.html")));
 | 
			
		||||
webuiRouter.get("/webui/riven-tool/", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(repoDir, "node_modules/warframe-riven-info/index.html"));
 | 
			
		||||
});
 | 
			
		||||
webuiRouter.get("/webui/riven-tool/RivenParser.js", async (_req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/javascript;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(repoDir, "node_modules/warframe-riven-info/RivenParser.js")));
 | 
			
		||||
webuiRouter.get("/webui/riven-tool/RivenParser.js", (_req, res) => {
 | 
			
		||||
    res.sendFile(path.join(repoDir, "node_modules/warframe-riven-info/RivenParser.js"));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Serve translations
 | 
			
		||||
webuiRouter.get("/translations/:file", async (req, res) => {
 | 
			
		||||
    res.set("Content-Type", "text/javascript;charset=utf8");
 | 
			
		||||
    res.send(await fs.readFile(path.join(baseDir, `static/webui/translations/${req.params.file}`)));
 | 
			
		||||
webuiRouter.get("/translations/:file", (req, res) => {
 | 
			
		||||
    res.sendFile(path.join(baseDir, `static/webui/translations/${req.params.file}`));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export { webuiRouter };
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@ export interface IConfig {
 | 
			
		||||
    bindAddress?: string;
 | 
			
		||||
    httpPort?: number;
 | 
			
		||||
    httpsPort?: number;
 | 
			
		||||
    ircExecutable?: string;
 | 
			
		||||
    ircAddress?: string;
 | 
			
		||||
    hubAddress?: string;
 | 
			
		||||
    nrsAddress?: string;
 | 
			
		||||
 | 
			
		||||
@ -1,425 +0,0 @@
 | 
			
		||||
import type { TFaction, TMissionType } from "warframe-public-export-plus";
 | 
			
		||||
import type { CalendarSeasonType, IConquest, IConquestMission, TConquestType } from "../types/worldStateTypes.ts";
 | 
			
		||||
import { mixSeeds, SRng } from "./rngService.ts";
 | 
			
		||||
import { EPOCH } from "../constants/timeConstants.ts";
 | 
			
		||||
 | 
			
		||||
const missionAndFactionTypes: Record<TConquestType, Partial<Record<TMissionType, TFaction[]>>> = {
 | 
			
		||||
    CT_LAB: {
 | 
			
		||||
        MT_EXTERMINATION: ["FC_MITW"],
 | 
			
		||||
        MT_SURVIVAL: ["FC_MITW"],
 | 
			
		||||
        MT_ALCHEMY: ["FC_MITW"],
 | 
			
		||||
        MT_DEFENSE: ["FC_MITW"],
 | 
			
		||||
        MT_ARTIFACT: ["FC_MITW"]
 | 
			
		||||
    },
 | 
			
		||||
    CT_HEX: {
 | 
			
		||||
        MT_EXTERMINATION: ["FC_SCALDRA", "FC_TECHROT"],
 | 
			
		||||
        MT_SURVIVAL: ["FC_SCALDRA", "FC_TECHROT"],
 | 
			
		||||
        MT_DEFENSE: ["FC_SCALDRA"],
 | 
			
		||||
        MT_ENDLESS_CAPTURE: ["FC_TECHROT"]
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const assassinationFactionOptions: Record<TConquestType, TFaction[]> = {
 | 
			
		||||
    CT_LAB: ["FC_MITW"],
 | 
			
		||||
    CT_HEX: ["FC_SCALDRA"]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type TConquestDifficulty = "CD_NORMAL" | "CD_HARD";
 | 
			
		||||
 | 
			
		||||
interface IConquestConditional {
 | 
			
		||||
    tag: string;
 | 
			
		||||
    missionType?: TMissionType;
 | 
			
		||||
    conquest?: TConquestType;
 | 
			
		||||
    difficulty?: TConquestDifficulty;
 | 
			
		||||
    season?: CalendarSeasonType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const deviations: readonly IConquestConditional[] = [
 | 
			
		||||
    {
 | 
			
		||||
        tag: "AlchemicalShields",
 | 
			
		||||
        missionType: "MT_ALCHEMY"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ContaminationZone",
 | 
			
		||||
        missionType: "MT_SURVIVAL",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "DoubleTrouble",
 | 
			
		||||
        missionType: "MT_ARTIFACT"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "EscalateImmediately",
 | 
			
		||||
        missionType: "MT_EXTERMINATION",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "EximusGrenadiers",
 | 
			
		||||
        missionType: "MT_ALCHEMY"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "FortifiedFoes",
 | 
			
		||||
        missionType: "MT_EXTERMINATION"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "FragileNodes",
 | 
			
		||||
        missionType: "MT_ARTIFACT"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "GrowingIncursion",
 | 
			
		||||
        missionType: "MT_EXTERMINATION",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "HarshWords",
 | 
			
		||||
        missionType: "MT_DEFENSE",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "HighScalingLegacyte",
 | 
			
		||||
        missionType: "MT_ENDLESS_CAPTURE",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "DoubleTroubleLegacyte",
 | 
			
		||||
        missionType: "MT_ENDLESS_CAPTURE",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "HostileSecurity",
 | 
			
		||||
        missionType: "MT_DEFENSE",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "InfiniteTide",
 | 
			
		||||
        missionType: "MT_ASSASSINATION",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "LostInTranslation",
 | 
			
		||||
        missionType: "MT_DEFENSE",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "MutatedEnemies",
 | 
			
		||||
        missionType: "MT_ENDLESS_CAPTURE",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "NecramechActivation",
 | 
			
		||||
        missionType: "MT_SURVIVAL",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "Reinforcements",
 | 
			
		||||
        missionType: "MT_ASSASSINATION",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "StickyFingers",
 | 
			
		||||
        missionType: "MT_ARTIFACT",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "TankStrongArmor",
 | 
			
		||||
        missionType: "MT_ASSASSINATION",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "TankReinforcements",
 | 
			
		||||
        missionType: "MT_ASSASSINATION",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "TankSuperToxic",
 | 
			
		||||
        missionType: "MT_ASSASSINATION",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "TechrotConjunction",
 | 
			
		||||
        missionType: "MT_SURVIVAL",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "UnpoweredCapsules",
 | 
			
		||||
        missionType: "MT_SURVIVAL",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "VolatileGrenades",
 | 
			
		||||
        missionType: "MT_ALCHEMY"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "GestatingTumors",
 | 
			
		||||
        missionType: "MT_SURVIVAL",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ChemicalNoise",
 | 
			
		||||
        missionType: "MT_DEFENSE",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ExplosiveEnergy",
 | 
			
		||||
        missionType: "MT_DEFENSE",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "DisruptiveSounds",
 | 
			
		||||
        missionType: "MT_DEFENSE",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const risks: readonly IConquestConditional[] = [
 | 
			
		||||
    {
 | 
			
		||||
        tag: "Voidburst"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "RegeneratingEnemies"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "VoidAberration"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ShieldedFoes"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "PointBlank"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "Deflectors",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "AcceleratedEnemies"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "DrainingResiduals"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "Quicksand"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "AntiMaterialWeapons",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ExplosiveCrawlers",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "EMPBlackHole",
 | 
			
		||||
        conquest: "CT_LAB"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ArtilleryBeacons",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "InfectedTechrot",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "BalloonFest",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "MiasmiteHive",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "CompetitionSpillover",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "HostileOvergrowth",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "MurmurIncursion",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "FactionSwarm_Techrot",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        difficulty: "CD_NORMAL"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "FactionSwarm_Scaldra",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        difficulty: "CD_NORMAL"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "HeavyWarfare",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ArcadeAutomata",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        difficulty: "CD_NORMAL"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "EfervonFog",
 | 
			
		||||
        conquest: "CT_HEX"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "WinterFrost",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        season: "CST_WINTER"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "JadeSpring",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        season: "CST_SPRING"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "ExplosiveSummer",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        season: "CST_SUMMER"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        tag: "FallFog",
 | 
			
		||||
        conquest: "CT_HEX",
 | 
			
		||||
        season: "CST_FALL"
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const filterConditionals = (
 | 
			
		||||
    arr: readonly IConquestConditional[],
 | 
			
		||||
    missionType: TMissionType | null,
 | 
			
		||||
    conquest: TConquestType | null,
 | 
			
		||||
    difficulty: TConquestDifficulty | null,
 | 
			
		||||
    season: CalendarSeasonType | null
 | 
			
		||||
): string[] => {
 | 
			
		||||
    const applicable = [];
 | 
			
		||||
    for (const cond of arr) {
 | 
			
		||||
        if (
 | 
			
		||||
            (!cond.missionType || cond.missionType == missionType) &&
 | 
			
		||||
            (!cond.conquest || cond.conquest == conquest) &&
 | 
			
		||||
            (!cond.difficulty || cond.difficulty == difficulty) &&
 | 
			
		||||
            (!cond.season || cond.season == season)
 | 
			
		||||
        ) {
 | 
			
		||||
            applicable.push(cond.tag);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return applicable;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const buildMission = (
 | 
			
		||||
    rng: SRng,
 | 
			
		||||
    conquest: TConquestType,
 | 
			
		||||
    missionType: TMissionType,
 | 
			
		||||
    faction: TFaction,
 | 
			
		||||
    season: CalendarSeasonType | null
 | 
			
		||||
): IConquestMission => {
 | 
			
		||||
    const deviation = rng.randomElement(filterConditionals(deviations, missionType, conquest, null, season))!;
 | 
			
		||||
    const easyRisk = rng.randomElement(filterConditionals(risks, missionType, conquest, "CD_NORMAL", season))!;
 | 
			
		||||
    const hardRiskOptions = filterConditionals(risks, missionType, conquest, "CD_HARD", season);
 | 
			
		||||
    {
 | 
			
		||||
        const i = hardRiskOptions.indexOf(easyRisk);
 | 
			
		||||
        if (i != -1) {
 | 
			
		||||
            hardRiskOptions.splice(i, 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    const hardRisk = rng.randomElement(hardRiskOptions)!;
 | 
			
		||||
    return {
 | 
			
		||||
        faction,
 | 
			
		||||
        missionType,
 | 
			
		||||
        difficulties: [
 | 
			
		||||
            {
 | 
			
		||||
                type: "CD_NORMAL",
 | 
			
		||||
                deviation,
 | 
			
		||||
                risks: [easyRisk]
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                type: "CD_HARD",
 | 
			
		||||
                deviation,
 | 
			
		||||
                risks: [easyRisk, hardRisk]
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const conquestStartingDay: Record<TConquestType, number> = {
 | 
			
		||||
    CT_LAB: 3703,
 | 
			
		||||
    CT_HEX: 4053
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// This function produces identical results to clients pre-40.0.0.
 | 
			
		||||
const getFrameVariables = (conquestType: TConquestType, time: number): [string, string, string, string] => {
 | 
			
		||||
    const day = Math.floor((time - 1391990400_000) / 86400_000) - conquestStartingDay[conquestType];
 | 
			
		||||
    const week = Math.floor(day / 7) + 1;
 | 
			
		||||
    const frameVariables = [
 | 
			
		||||
        "Framecurse",
 | 
			
		||||
        "Knifestep",
 | 
			
		||||
        "Exhaustion",
 | 
			
		||||
        "Gearless",
 | 
			
		||||
        "TimeDilation",
 | 
			
		||||
        "Armorless",
 | 
			
		||||
        "Starvation",
 | 
			
		||||
        "ShieldDelay",
 | 
			
		||||
        "Withering",
 | 
			
		||||
        "ContactDamage",
 | 
			
		||||
        "AbilityLockout",
 | 
			
		||||
        "OperatorLockout",
 | 
			
		||||
        "EnergyStarved",
 | 
			
		||||
        "OverSensitive",
 | 
			
		||||
        "AntiGuard",
 | 
			
		||||
        "DecayingFlesh",
 | 
			
		||||
        "VoidEnergyOverload",
 | 
			
		||||
        "DullBlades",
 | 
			
		||||
        "Undersupplied"
 | 
			
		||||
    ];
 | 
			
		||||
    const mag = Math.floor(frameVariables.length / 4);
 | 
			
		||||
    const rng = new SRng(conquestStartingDay[conquestType] + Math.floor(week / mag));
 | 
			
		||||
    rng.shuffleArray(frameVariables);
 | 
			
		||||
    const i = week % mag;
 | 
			
		||||
    return [frameVariables[i], frameVariables[i + 1], frameVariables[i + 2], frameVariables[i + 3]];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getConquest = (
 | 
			
		||||
    conquestType: TConquestType,
 | 
			
		||||
    week: number,
 | 
			
		||||
    season: CalendarSeasonType | null
 | 
			
		||||
): IConquest => {
 | 
			
		||||
    const rng = new SRng(mixSeeds(conquestStartingDay[conquestType], week));
 | 
			
		||||
 | 
			
		||||
    const missions: IConquestMission[] = [];
 | 
			
		||||
    {
 | 
			
		||||
        const missionOptions = Object.entries(missionAndFactionTypes[conquestType]);
 | 
			
		||||
        {
 | 
			
		||||
            const i = rng.randomInt(0, missionOptions.length - 1);
 | 
			
		||||
            const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
 | 
			
		||||
            missions.push(
 | 
			
		||||
                buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        {
 | 
			
		||||
            const i = rng.randomInt(0, missionOptions.length - 1);
 | 
			
		||||
            const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
 | 
			
		||||
            missions.push(
 | 
			
		||||
                buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        missionOptions.push(["MT_ASSASSINATION", assassinationFactionOptions[conquestType]]);
 | 
			
		||||
        {
 | 
			
		||||
            const i = rng.randomInt(0, missionOptions.length - 1);
 | 
			
		||||
            const [missionType, factionOptions] = missionOptions.splice(i, 1)[0];
 | 
			
		||||
            missions.push(
 | 
			
		||||
                buildMission(rng, conquestType, missionType as TMissionType, rng.randomElement(factionOptions)!, season)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const weekStart = EPOCH + week * 604800000;
 | 
			
		||||
    const weekEnd = weekStart + 604800000;
 | 
			
		||||
    return {
 | 
			
		||||
        Activation: { $date: { $numberLong: weekStart.toString() } },
 | 
			
		||||
        Expiry: { $date: { $numberLong: weekEnd.toString() } },
 | 
			
		||||
        Type: conquestType,
 | 
			
		||||
        Missions: missions,
 | 
			
		||||
        Variables: getFrameVariables(conquestType, weekStart),
 | 
			
		||||
        RandomSeed: rng.randomInt(0, 1_000_000)
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@ -116,8 +116,8 @@ export const createInventory = async (
 | 
			
		||||
        if (config.skipTutorial) {
 | 
			
		||||
            inventory.PlayedParkourTutorial = true;
 | 
			
		||||
            await addStartingGear(inventory);
 | 
			
		||||
            await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain", undefined);
 | 
			
		||||
            await completeQuest(inventory, "/Lotus/Types/Keys/ModQuest/ModQuestKeyChain", undefined);
 | 
			
		||||
            await completeQuest(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain");
 | 
			
		||||
            await completeQuest(inventory, "/Lotus/Types/Keys/ModQuest/ModQuestKeyChain");
 | 
			
		||||
 | 
			
		||||
            const completedMissions = ["SolNode27", "SolNode89", "SolNode63", "SolNode85", "SolNode15", "SolNode79"];
 | 
			
		||||
 | 
			
		||||
@ -473,9 +473,7 @@ export const addItem = async (
 | 
			
		||||
        return addCustomization(inventory, typeName);
 | 
			
		||||
    }
 | 
			
		||||
    if (typeName in ExportUpgrades || typeName in ExportArcanes) {
 | 
			
		||||
        if (targetFingerprint && typeName.startsWith("/Lotus/Upgrades/Mods/Randomized/Raw")) {
 | 
			
		||||
            logger.debug(`ignoring fingerprint for raw riven mod`);
 | 
			
		||||
        } else if (targetFingerprint) {
 | 
			
		||||
        if (targetFingerprint) {
 | 
			
		||||
            if (quantity != 1) {
 | 
			
		||||
                logger.warn(`adding 1 of ${typeName} ${targetFingerprint} even tho quantity ${quantity} was requested`);
 | 
			
		||||
            }
 | 
			
		||||
@ -1393,10 +1391,7 @@ export const addStanding = (
 | 
			
		||||
 | 
			
		||||
// TODO: AffiliationMods support (Nightwave).
 | 
			
		||||
export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<IUpdateNodeIntrosResponse> => {
 | 
			
		||||
    const inventory = await getInventory(
 | 
			
		||||
        accountId,
 | 
			
		||||
        "NodeIntrosCompleted MiscItems ShipDecorations FlavourItems WeaponSkins"
 | 
			
		||||
    );
 | 
			
		||||
    const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems ShipDecorations");
 | 
			
		||||
 | 
			
		||||
    // Make it an array for easier parsing.
 | 
			
		||||
    if (typeof data.NodeIntrosCompleted === "string") {
 | 
			
		||||
@ -1405,12 +1400,6 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    for (const node of data.NodeIntrosCompleted) {
 | 
			
		||||
        if (inventory.NodeIntrosCompleted.indexOf(node) != -1) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        inventory.NodeIntrosCompleted.push(node);
 | 
			
		||||
        logger.debug(`completed dialogue/cutscene for ${node}`);
 | 
			
		||||
 | 
			
		||||
        if (node == "TC2025") {
 | 
			
		||||
            inventoryChanges.ShipDecorations = [
 | 
			
		||||
                {
 | 
			
		||||
@ -1431,15 +1420,16 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr
 | 
			
		||||
            await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/BeatCaliberChicksEmailItem", inventoryChanges);
 | 
			
		||||
        } else if (node == "ClearedFiveLoops") {
 | 
			
		||||
            await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/ClearedFiveLoopsEmailItem", inventoryChanges);
 | 
			
		||||
        } else if (node == "NokkoVisions_AllCompleted") {
 | 
			
		||||
            addCustomization(
 | 
			
		||||
                inventory,
 | 
			
		||||
                "/Lotus/Types/StoreItems/AvatarImages/Warframes/NokkoBabySecretGlyph",
 | 
			
		||||
                inventoryChanges
 | 
			
		||||
            );
 | 
			
		||||
            addSkin(inventory, "/Lotus/Upgrades/Skins/Clan/NokkoBabySecretEmblemItem", inventoryChanges);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Combine the two arrays into one.
 | 
			
		||||
    data.NodeIntrosCompleted = inventory.NodeIntrosCompleted.concat(data.NodeIntrosCompleted);
 | 
			
		||||
 | 
			
		||||
    // Remove duplicate entries.
 | 
			
		||||
    const nodes = [...new Set(data.NodeIntrosCompleted)];
 | 
			
		||||
 | 
			
		||||
    inventory.NodeIntrosCompleted = nodes;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
@ -1571,7 +1561,6 @@ const addCrewShip = async (
 | 
			
		||||
            const questChanges = await completeQuest(
 | 
			
		||||
                inventory,
 | 
			
		||||
                "/Lotus/Types/Keys/RailJackBuildQuest/RailjackBuildQuestKeyChain",
 | 
			
		||||
                undefined,
 | 
			
		||||
                false
 | 
			
		||||
            );
 | 
			
		||||
            if (questChanges) {
 | 
			
		||||
@ -2183,7 +2172,7 @@ export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag,
 | 
			
		||||
 | 
			
		||||
    if (itemIndex !== -1) {
 | 
			
		||||
        Missions[itemIndex].Completes += Completes;
 | 
			
		||||
        if (Completes && Tier) {
 | 
			
		||||
        if (Tier) {
 | 
			
		||||
            Missions[itemIndex].Tier = Tier;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
@ -40,8 +40,6 @@ import {
 | 
			
		||||
} from "warframe-public-export-plus";
 | 
			
		||||
import type { IMessage } from "../models/inboxModel.ts";
 | 
			
		||||
import { logger } from "../utils/logger.ts";
 | 
			
		||||
import { version_compare } from "../helpers/inventoryHelpers.ts";
 | 
			
		||||
import vorsPrizePreU40Rewards from "../../static/fixed_responses/vorsPrizePreU40Rewards.json" with { type: "json" };
 | 
			
		||||
 | 
			
		||||
export type WeaponTypeInternal =
 | 
			
		||||
    | "LongGuns"
 | 
			
		||||
@ -229,13 +227,12 @@ export const getKeyChainItems = ({ KeyChain, ChainStage }: IKeyChainRequest): st
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getLevelKeyRewards = (
 | 
			
		||||
    levelKey: string,
 | 
			
		||||
    buildLabel: string | undefined
 | 
			
		||||
    levelKey: string
 | 
			
		||||
): { levelKeyRewards?: IMissionReward; levelKeyRewards2?: TReward[] } => {
 | 
			
		||||
    const key = ExportKeys[levelKey] as IKey | undefined;
 | 
			
		||||
 | 
			
		||||
    const levelKeyRewards = key?.missionReward;
 | 
			
		||||
    let levelKeyRewards2 = key?.rewards;
 | 
			
		||||
    const levelKeyRewards2 = key?.rewards;
 | 
			
		||||
 | 
			
		||||
    if (!levelKeyRewards && !levelKeyRewards2) {
 | 
			
		||||
        logger.warn(
 | 
			
		||||
@ -243,12 +240,6 @@ export const getLevelKeyRewards = (
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (buildLabel && version_compare(buildLabel, "2025.10.14.16.10") < 0) {
 | 
			
		||||
        if (levelKey in vorsPrizePreU40Rewards) {
 | 
			
		||||
            levelKeyRewards2 = vorsPrizePreU40Rewards[levelKey as keyof typeof vorsPrizePreU40Rewards] as TReward[];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        levelKeyRewards,
 | 
			
		||||
        levelKeyRewards2
 | 
			
		||||
 | 
			
		||||
@ -41,8 +41,8 @@ export const createAccount = async (accountData: IDatabaseAccountRequiredFields)
 | 
			
		||||
        await account.save();
 | 
			
		||||
        const loadoutId = await createLoadout(account._id);
 | 
			
		||||
        const shipId = await createShip(account._id);
 | 
			
		||||
        await createPersonalRooms(account._id, shipId);
 | 
			
		||||
        await createInventory(account._id, { loadOutPresetId: loadoutId, ship: shipId });
 | 
			
		||||
        await createPersonalRooms(account._id, shipId);
 | 
			
		||||
        await createStats(account._id.toString());
 | 
			
		||||
        return account.toJSON();
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
@ -64,6 +64,17 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ
 | 
			
		||||
        personalRoomsOwnerId: accountId,
 | 
			
		||||
        activeShipId: shipId
 | 
			
		||||
    });
 | 
			
		||||
    if (config.skipTutorial) {
 | 
			
		||||
        // unlocked during Vor's Prize and The Teacher quests
 | 
			
		||||
        const defaultFeatures = [
 | 
			
		||||
            "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem",
 | 
			
		||||
            "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem",
 | 
			
		||||
            "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem",
 | 
			
		||||
            "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem",
 | 
			
		||||
            "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem"
 | 
			
		||||
        ];
 | 
			
		||||
        personalRooms.Ship.Features.push(...defaultFeatures);
 | 
			
		||||
    }
 | 
			
		||||
    await personalRooms.save();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -243,7 +243,7 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (inventoryUpdates.RewardInfo.jobId) {
 | 
			
		||||
        if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) {
 | 
			
		||||
            // e.g. for Profit-Taker Phase 1:
 | 
			
		||||
            // JobTier: -6,
 | 
			
		||||
            // jobId: '/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne_-6_SolarisUnitedHub1_663a71c80000000000000025_EudicoHeists',
 | 
			
		||||
@ -251,10 +251,7 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
 | 
			
		||||
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
            const [bounty, tier, hub, id, tag] = inventoryUpdates.RewardInfo.jobId.split("_");
 | 
			
		||||
            if (
 | 
			
		||||
                (tag == "EudicoHeists" && inventoryUpdates.MissionStatus == "GS_SUCCESS") ||
 | 
			
		||||
                (tag == "NokkoColony" && inventoryUpdates.RewardInfo.JobStage == 4)
 | 
			
		||||
            ) {
 | 
			
		||||
            if (tag == "EudicoHeists") {
 | 
			
		||||
                inventory.CompletedJobChains ??= [];
 | 
			
		||||
                let chain = inventory.CompletedJobChains.find(x => x.LocationTag == tag);
 | 
			
		||||
                if (!chain) {
 | 
			
		||||
@ -313,9 +310,6 @@ export const addMissionInventoryUpdates = async (
 | 
			
		||||
                }
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
            case "Missions":
 | 
			
		||||
                addMissionComplete(inventory, value);
 | 
			
		||||
                break;
 | 
			
		||||
            case "LastRegionPlayed":
 | 
			
		||||
                if (!(config.unfaithfulBugFixes?.ignore1999LastRegionPlayed && value === "1999MapName")) {
 | 
			
		||||
                    inventory.LastRegionPlayed = value;
 | 
			
		||||
@ -1133,8 +1127,7 @@ export const addMissionRewards = async (
 | 
			
		||||
        VoidTearParticipantsCurrWave: voidTearWave,
 | 
			
		||||
        StrippedItems: strippedItems,
 | 
			
		||||
        AffiliationChanges: AffiliationMods,
 | 
			
		||||
        InvasionProgress: invasionProgress,
 | 
			
		||||
        EndOfMatchUpload: endOfMatchUpload
 | 
			
		||||
        InvasionProgress: invasionProgress
 | 
			
		||||
    }: IMissionInventoryUpdateRequest,
 | 
			
		||||
    firstCompletion: boolean
 | 
			
		||||
): Promise<AddMissionRewardsReturnType> => {
 | 
			
		||||
@ -1219,7 +1212,7 @@ export const addMissionRewards = async (
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (levelKeyName) {
 | 
			
		||||
        const fixedLevelRewards = getLevelKeyRewards(levelKeyName, account.BuildLabel);
 | 
			
		||||
        const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
 | 
			
		||||
        //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
 | 
			
		||||
        if (fixedLevelRewards.levelKeyRewards) {
 | 
			
		||||
            missionCompletionCredits += addFixedLevelRewards(
 | 
			
		||||
@ -1249,6 +1242,9 @@ export const addMissionRewards = async (
 | 
			
		||||
    if (missions && missions.Tag in ExportRegions) {
 | 
			
		||||
        const node = ExportRegions[missions.Tag];
 | 
			
		||||
 | 
			
		||||
        // cannot add this with normal updates because { Tier: 1 } would mark the SP node as completed even on a failure
 | 
			
		||||
        addMissionComplete(inventory, missions);
 | 
			
		||||
 | 
			
		||||
        //node based credit rewards for mission completion
 | 
			
		||||
        if (isEligibleForCreditReward(rewardInfo, missions, node)) {
 | 
			
		||||
            const levelCreditReward = getLevelCreditRewards(node);
 | 
			
		||||
@ -1393,77 +1389,73 @@ export const addMissionRewards = async (
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (strippedItems) {
 | 
			
		||||
        if (endOfMatchUpload) {
 | 
			
		||||
            for (const si of strippedItems) {
 | 
			
		||||
                if (si.DropTable in droptableAliases) {
 | 
			
		||||
                    logger.debug(`rewriting ${si.DropTable} to ${droptableAliases[si.DropTable]}`);
 | 
			
		||||
                    si.DropTable = droptableAliases[si.DropTable];
 | 
			
		||||
                }
 | 
			
		||||
                const droptables = ExportEnemies.droptables[si.DropTable] ?? [];
 | 
			
		||||
                if (si.DROP_MOD) {
 | 
			
		||||
                    const modDroptable = droptables.find(x => x.type == "mod");
 | 
			
		||||
                    if (modDroptable) {
 | 
			
		||||
                        for (let i = 0; i != si.DROP_MOD.length; ++i) {
 | 
			
		||||
                            const reward = getRandomReward(modDroptable.items)!;
 | 
			
		||||
                            logger.debug(`stripped droptable (mods pool) rolled`, reward);
 | 
			
		||||
                            await addItem(inventory, reward.type);
 | 
			
		||||
                            MissionRewards.push({
 | 
			
		||||
                                StoreItem: toStoreItem(reward.type),
 | 
			
		||||
                                ItemCount: 1,
 | 
			
		||||
                                FromEnemyCache: true // to show "identified"
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        logger.error(`unknown droptable ${si.DropTable} for DROP_MOD`);
 | 
			
		||||
        for (const si of strippedItems) {
 | 
			
		||||
            if (si.DropTable in droptableAliases) {
 | 
			
		||||
                logger.debug(`rewriting ${si.DropTable} to ${droptableAliases[si.DropTable]}`);
 | 
			
		||||
                si.DropTable = droptableAliases[si.DropTable];
 | 
			
		||||
            }
 | 
			
		||||
            const droptables = ExportEnemies.droptables[si.DropTable] ?? [];
 | 
			
		||||
            if (si.DROP_MOD) {
 | 
			
		||||
                const modDroptable = droptables.find(x => x.type == "mod");
 | 
			
		||||
                if (modDroptable) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_MOD.length; ++i) {
 | 
			
		||||
                        const reward = getRandomReward(modDroptable.items)!;
 | 
			
		||||
                        logger.debug(`stripped droptable (mods pool) rolled`, reward);
 | 
			
		||||
                        await addItem(inventory, reward.type);
 | 
			
		||||
                        MissionRewards.push({
 | 
			
		||||
                            StoreItem: toStoreItem(reward.type),
 | 
			
		||||
                            ItemCount: 1,
 | 
			
		||||
                            FromEnemyCache: true // to show "identified"
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (si.DROP_BLUEPRINT) {
 | 
			
		||||
                    const blueprintDroptable = droptables.find(x => x.type == "blueprint");
 | 
			
		||||
                    if (blueprintDroptable) {
 | 
			
		||||
                        for (let i = 0; i != si.DROP_BLUEPRINT.length; ++i) {
 | 
			
		||||
                            const reward = getRandomReward(blueprintDroptable.items)!;
 | 
			
		||||
                            logger.debug(`stripped droptable (blueprints pool) rolled`, reward);
 | 
			
		||||
                            await addItem(inventory, reward.type);
 | 
			
		||||
                            MissionRewards.push({
 | 
			
		||||
                                StoreItem: toStoreItem(reward.type),
 | 
			
		||||
                                ItemCount: 1,
 | 
			
		||||
                                FromEnemyCache: true // to show "identified"
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        logger.error(`unknown droptable ${si.DropTable} for DROP_BLUEPRINT`);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                // e.g. H-09 Apex Turret Sumdali
 | 
			
		||||
                if (si.DROP_MISC_ITEM) {
 | 
			
		||||
                    const resourceDroptable = droptables.find(x => x.type == "resource");
 | 
			
		||||
                    if (resourceDroptable) {
 | 
			
		||||
                        for (let i = 0; i != si.DROP_MISC_ITEM.length; ++i) {
 | 
			
		||||
                            const reward = getRandomReward(resourceDroptable.items)!;
 | 
			
		||||
                            logger.debug(`stripped droptable (resources pool) rolled`, reward);
 | 
			
		||||
                            if (Object.keys(await addItem(inventory, reward.type)).length == 0) {
 | 
			
		||||
                                logger.debug(`item already owned, skipping`);
 | 
			
		||||
                            } else {
 | 
			
		||||
                                MissionRewards.push({
 | 
			
		||||
                                    StoreItem: toStoreItem(reward.type),
 | 
			
		||||
                                    ItemCount: 1,
 | 
			
		||||
                                    FromEnemyCache: true // to show "identified"
 | 
			
		||||
                                });
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        logger.error(`unknown droptable ${si.DropTable} for DROP_MISC_ITEM`);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (si.DropTable == "/Lotus/Types/DropTables/ContainerDropTables/VoidVaultMissionRewardsDropTable") {
 | 
			
		||||
                    // Consume netracells search pulse; only when the container reward was picked up. Discussed in https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2673
 | 
			
		||||
                    updateEntratiVault(inventory);
 | 
			
		||||
                    inventory.EntratiVaultCountLastPeriod! += 1;
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.error(`unknown droptable ${si.DropTable} for DROP_MOD`);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            logger.debug(`ignoring StrippedItems in intermediate inventory update, deferring until extraction`);
 | 
			
		||||
            if (si.DROP_BLUEPRINT) {
 | 
			
		||||
                const blueprintDroptable = droptables.find(x => x.type == "blueprint");
 | 
			
		||||
                if (blueprintDroptable) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_BLUEPRINT.length; ++i) {
 | 
			
		||||
                        const reward = getRandomReward(blueprintDroptable.items)!;
 | 
			
		||||
                        logger.debug(`stripped droptable (blueprints pool) rolled`, reward);
 | 
			
		||||
                        await addItem(inventory, reward.type);
 | 
			
		||||
                        MissionRewards.push({
 | 
			
		||||
                            StoreItem: toStoreItem(reward.type),
 | 
			
		||||
                            ItemCount: 1,
 | 
			
		||||
                            FromEnemyCache: true // to show "identified"
 | 
			
		||||
                        });
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.error(`unknown droptable ${si.DropTable} for DROP_BLUEPRINT`);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            // e.g. H-09 Apex Turret Sumdali
 | 
			
		||||
            if (si.DROP_MISC_ITEM) {
 | 
			
		||||
                const resourceDroptable = droptables.find(x => x.type == "resource");
 | 
			
		||||
                if (resourceDroptable) {
 | 
			
		||||
                    for (let i = 0; i != si.DROP_MISC_ITEM.length; ++i) {
 | 
			
		||||
                        const reward = getRandomReward(resourceDroptable.items)!;
 | 
			
		||||
                        logger.debug(`stripped droptable (resources pool) rolled`, reward);
 | 
			
		||||
                        if (Object.keys(await addItem(inventory, reward.type)).length == 0) {
 | 
			
		||||
                            logger.debug(`item already owned, skipping`);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            MissionRewards.push({
 | 
			
		||||
                                StoreItem: toStoreItem(reward.type),
 | 
			
		||||
                                ItemCount: 1,
 | 
			
		||||
                                FromEnemyCache: true // to show "identified"
 | 
			
		||||
                            });
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.error(`unknown droptable ${si.DropTable} for DROP_MISC_ITEM`);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (si.DropTable == "/Lotus/Types/DropTables/ContainerDropTables/VoidVaultMissionRewardsDropTable") {
 | 
			
		||||
                // Consume netracells search pulse; only when the container reward was picked up. Discussed in https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2673
 | 
			
		||||
                updateEntratiVault(inventory);
 | 
			
		||||
                inventory.EntratiVaultCountLastPeriod! += 1;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1522,7 +1514,7 @@ export const addMissionRewards = async (
 | 
			
		||||
            syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
 | 
			
		||||
            if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
 | 
			
		||||
        }
 | 
			
		||||
        if (syndicateEntry && syndicateEntry.Jobs && !jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs")) {
 | 
			
		||||
        if (syndicateEntry && syndicateEntry.Jobs) {
 | 
			
		||||
            let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
 | 
			
		||||
            if (
 | 
			
		||||
                [
 | 
			
		||||
@ -2030,19 +2022,6 @@ function getRandomMissionDrops(
 | 
			
		||||
                                    xpAmounts: [1000]
 | 
			
		||||
                                };
 | 
			
		||||
                                RewardInfo.Q = false; // Just in case
 | 
			
		||||
                            } else if (jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob")) {
 | 
			
		||||
                                job = {
 | 
			
		||||
                                    rewards: jobType
 | 
			
		||||
                                        .replace("SteelPath", "Steel")
 | 
			
		||||
                                        .replace(
 | 
			
		||||
                                            "/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob",
 | 
			
		||||
                                            "/Lotus/Types/Game/MissionDecks/NokkoColonyRewards/NokkoColonyRewards"
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    masteryReq: 0,
 | 
			
		||||
                                    minEnemyLevel: 30,
 | 
			
		||||
                                    maxEnemyLevel: 40,
 | 
			
		||||
                                    xpAmounts: [0, 0, 0, 0, 0]
 | 
			
		||||
                                };
 | 
			
		||||
                            } else {
 | 
			
		||||
                                const tierMap = {
 | 
			
		||||
                                    Two: "B",
 | 
			
		||||
@ -2086,22 +2065,14 @@ function getRandomMissionDrops(
 | 
			
		||||
                            } else if (totalStage == 5 && curentStage == 4) {
 | 
			
		||||
                                tableIndex = 2;
 | 
			
		||||
                            }
 | 
			
		||||
                            if (jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob")) {
 | 
			
		||||
                                if (RewardInfo.JobStage === job.xpAmounts.length - 1) {
 | 
			
		||||
                                    rotations = [0, 1, 2];
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    rewardManifests = [];
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                rotations = [tableIndex];
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            rotations = [tableIndex];
 | 
			
		||||
                        } else {
 | 
			
		||||
                            rotations = [0];
 | 
			
		||||
                        }
 | 
			
		||||
                        if (
 | 
			
		||||
                            RewardInfo.Q &&
 | 
			
		||||
                            (RewardInfo.JobStage === job.xpAmounts.length - 1 || jobType.endsWith("VaultBounty")) &&
 | 
			
		||||
                            !jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob") &&
 | 
			
		||||
                            !isEndlessJob
 | 
			
		||||
                        ) {
 | 
			
		||||
                            rotations.push(ExportRewards[job.rewards].length - 1);
 | 
			
		||||
 | 
			
		||||
@ -2,25 +2,15 @@ import type { IKeyChainRequest } from "../types/requestTypes.ts";
 | 
			
		||||
import { isEmptyObject } from "../helpers/general.ts";
 | 
			
		||||
import type { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { createMessage } from "./inboxService.ts";
 | 
			
		||||
import {
 | 
			
		||||
    addEquipment,
 | 
			
		||||
    addItem,
 | 
			
		||||
    addItems,
 | 
			
		||||
    addKeyChainItems,
 | 
			
		||||
    addPowerSuit,
 | 
			
		||||
    setupKahlSyndicate
 | 
			
		||||
} from "./inventoryService.ts";
 | 
			
		||||
import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "./inventoryService.ts";
 | 
			
		||||
import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "./itemDataService.ts";
 | 
			
		||||
import type { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { logger } from "../utils/logger.ts";
 | 
			
		||||
import { ExportKeys, ExportRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportKeys } from "warframe-public-export-plus";
 | 
			
		||||
import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../types/purchaseTypes.ts";
 | 
			
		||||
import questCompletionItems from "../../static/fixed_responses/questCompletionRewards.json" with { type: "json" };
 | 
			
		||||
import type { ITypeCount } from "../types/commonTypes.ts";
 | 
			
		||||
import { addString } from "../helpers/stringHelpers.ts";
 | 
			
		||||
import { unlockShipFeature } from "./personalRoomsService.ts";
 | 
			
		||||
import { EquipmentFeatures } from "../types/equipmentTypes.ts";
 | 
			
		||||
 | 
			
		||||
export interface IUpdateQuestRequest {
 | 
			
		||||
    QuestKeys: IQuestKeyClient[];
 | 
			
		||||
@ -101,26 +91,6 @@ export const updateQuestStage = (
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const resetQuestKeyToStage = (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    { KeyChain, ChainStage }: IKeyChainRequest
 | 
			
		||||
): void => {
 | 
			
		||||
    const quest = inventory.QuestKeys.find(quest => quest.ItemType === KeyChain);
 | 
			
		||||
 | 
			
		||||
    if (!quest) {
 | 
			
		||||
        throw new Error(`Quest ${KeyChain} not found in QuestKeys`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    quest.Progress ??= [];
 | 
			
		||||
 | 
			
		||||
    const run = quest.Progress[0]?.c ?? 0;
 | 
			
		||||
    if (run >= 0) {
 | 
			
		||||
        for (let i = ChainStage; i < quest.Progress.length; ++i) {
 | 
			
		||||
            quest.Progress[i].c = run - 1;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const addQuestKey = (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    questKey: IQuestKeyDatabase
 | 
			
		||||
@ -149,7 +119,6 @@ export const addQuestKey = (
 | 
			
		||||
export const completeQuest = async (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    questKey: string,
 | 
			
		||||
    buildLabel: string | undefined,
 | 
			
		||||
    sendMessages: boolean = false
 | 
			
		||||
): Promise<void | IQuestKeyClient> => {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
@ -198,8 +167,7 @@ export const completeQuest = async (
 | 
			
		||||
        if (stage.c <= run) {
 | 
			
		||||
            stage.c = run;
 | 
			
		||||
            await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i }, sendMessages);
 | 
			
		||||
            await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }, buildLabel);
 | 
			
		||||
            await installShipFeatures(inventory, { KeyChain: questKey, ChainStage: i }, buildLabel);
 | 
			
		||||
            await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -207,11 +175,6 @@ export const completeQuest = async (
 | 
			
		||||
        existingQuestKey.Completed = true;
 | 
			
		||||
        existingQuestKey.CompletionDate = new Date();
 | 
			
		||||
        await handleQuestCompletion(inventory, questKey, undefined, run > 0);
 | 
			
		||||
 | 
			
		||||
        if (questKey == "/Lotus/Types/Keys/ModQuest/ModQuestKeyChain") {
 | 
			
		||||
            // This would be set by the client during the equilogue, but since we're cheating through, we have to do it ourselves.
 | 
			
		||||
            addString(inventory.NodeIntrosCompleted, "ModQuestTeshinAccess");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return existingQuestKey.toJSON<IQuestKeyClient>();
 | 
			
		||||
@ -336,9 +299,6 @@ const handleQuestCompletion = async (
 | 
			
		||||
    logger.debug(`quest completion items`, questCompletionItems);
 | 
			
		||||
    if (questCompletionItems) {
 | 
			
		||||
        await addItems(inventory, questCompletionItems, inventoryChanges);
 | 
			
		||||
        for (const item of questCompletionItems) {
 | 
			
		||||
            await removeRequiredItems(inventory, item.ItemType);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -388,12 +348,7 @@ export const giveKeyChainMessage = async (
 | 
			
		||||
        await createMessage(inventory.accountOwnerId, [keyChainMessage]);
 | 
			
		||||
    } else {
 | 
			
		||||
        if (keyChainMessage.countedAtt?.length) await addItems(inventory, keyChainMessage.countedAtt);
 | 
			
		||||
        if (keyChainMessage.att?.length) {
 | 
			
		||||
            await addItems(inventory, keyChainMessage.att);
 | 
			
		||||
            for (const reward of keyChainMessage.att) {
 | 
			
		||||
                await removeRequiredItems(inventory, reward);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (keyChainMessage.att?.length) await addItems(inventory, keyChainMessage.att);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    updateQuestStage(inventory, keyChainInfo, { m: true });
 | 
			
		||||
@ -401,8 +356,7 @@ export const giveKeyChainMessage = async (
 | 
			
		||||
 | 
			
		||||
export const giveKeyChainMissionReward = async (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    keyChainInfo: IKeyChainRequest,
 | 
			
		||||
    buildLabel: string | undefined
 | 
			
		||||
    keyChainInfo: IKeyChainRequest
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
    const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages;
 | 
			
		||||
@ -411,7 +365,7 @@ export const giveKeyChainMissionReward = async (
 | 
			
		||||
        const missionName = chainStages[keyChainInfo.ChainStage].key;
 | 
			
		||||
        const questKey = inventory.QuestKeys.find(q => q.ItemType === keyChainInfo.KeyChain);
 | 
			
		||||
        if (missionName && questKey) {
 | 
			
		||||
            const fixedLevelRewards = getLevelKeyRewards(missionName, buildLabel);
 | 
			
		||||
            const fixedLevelRewards = getLevelKeyRewards(missionName);
 | 
			
		||||
            const run = questKey.Progress?.[0]?.c ?? 0;
 | 
			
		||||
            if (fixedLevelRewards.levelKeyRewards) {
 | 
			
		||||
                const missionRewards: { StoreItem: string; ItemCount: number }[] = [];
 | 
			
		||||
@ -419,7 +373,6 @@ export const giveKeyChainMissionReward = async (
 | 
			
		||||
 | 
			
		||||
                for (const reward of missionRewards) {
 | 
			
		||||
                    await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount);
 | 
			
		||||
                    await removeRequiredItems(inventory, fromStoreItem(reward.StoreItem));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                updateQuestStage(inventory, keyChainInfo, { c: run });
 | 
			
		||||
@ -433,7 +386,6 @@ export const giveKeyChainMissionReward = async (
 | 
			
		||||
                        await addItem(inventory, fromStoreItem(reward.itemType), reward.amount);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        await addItem(inventory, fromStoreItem(reward.itemType));
 | 
			
		||||
                        await removeRequiredItems(inventory, fromStoreItem(reward.itemType));
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
@ -462,338 +414,3 @@ export const giveKeyChainStageTriggered = async (
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const installShipFeatures = async (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    keyChainInfo: IKeyChainRequest,
 | 
			
		||||
    buildLabel: string | undefined
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
    const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages;
 | 
			
		||||
    const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain);
 | 
			
		||||
    if (chainStages && questKey) {
 | 
			
		||||
        if (keyChainInfo.ChainStage - 1 >= 0) {
 | 
			
		||||
            const prevStage = chainStages[keyChainInfo.ChainStage - 1];
 | 
			
		||||
            for (const item of prevStage.itemsToGiveWhenTriggered) {
 | 
			
		||||
                if (item.startsWith("/Lotus/StoreItems/Types/Items/ShipFeatureItems/")) {
 | 
			
		||||
                    logger.debug(`installing ship feature ${fromStoreItem(item)}`);
 | 
			
		||||
                    await unlockShipFeature(inventory, fromStoreItem(item));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (prevStage.key) {
 | 
			
		||||
                const fixedLevelRewards = getLevelKeyRewards(prevStage.key, buildLabel);
 | 
			
		||||
                if (fixedLevelRewards.levelKeyRewards?.items) {
 | 
			
		||||
                    for (const item of fixedLevelRewards.levelKeyRewards.items) {
 | 
			
		||||
                        if (item.startsWith("/Lotus/StoreItems/Types/Items/ShipFeatureItems/")) {
 | 
			
		||||
                            logger.debug(`installing ship feature ${fromStoreItem(item)}`);
 | 
			
		||||
                            await unlockShipFeature(inventory, fromStoreItem(item));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                if (fixedLevelRewards.levelKeyRewards2) {
 | 
			
		||||
                    for (const item of fixedLevelRewards.levelKeyRewards2) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            item.rewardType == "RT_STORE_ITEM" &&
 | 
			
		||||
                            item.itemType.startsWith("/Lotus/StoreItems/Types/Items/ShipFeatureItems/")
 | 
			
		||||
                        ) {
 | 
			
		||||
                            logger.debug(`installing ship feature ${fromStoreItem(item.itemType)}`);
 | 
			
		||||
                            await unlockShipFeature(inventory, fromStoreItem(item.itemType));
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const removeRequiredItems = async (inventory: TInventoryDatabaseDocument, typeName: string): Promise<void> => {
 | 
			
		||||
    switch (typeName) {
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Keys/LimboQuest/LimboHelmetKeyBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Keys/LimboQuest/LimboSystemsKeyBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Keys/LimboQuest/LimboChassisKeyBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/BrawlerBlueprint": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Recipes/Components/InfestedIrradiatedBaitBallBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    await addItem(inventory, recipe.resultType, -1, false, undefined, undefined, true);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Recipes/ArchwingRecipes/StandardArchwing/StandardArchwingBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, recipeItem.ItemCount * -1);
 | 
			
		||||
                    await addItems(inventory, [
 | 
			
		||||
                        {
 | 
			
		||||
                            ItemType:
 | 
			
		||||
                                "/Lotus/Types/Recipes/ArchwingRecipes/StandardArchwing/StandardArchwingWingsBlueprint",
 | 
			
		||||
                            ItemCount: -1
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            ItemType:
 | 
			
		||||
                                "/Lotus/Types/Recipes/ArchwingRecipes/StandardArchwing/StandardArchwingChassisBlueprint",
 | 
			
		||||
                            ItemCount: -1
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            ItemType:
 | 
			
		||||
                                "/Lotus/Types/Recipes/ArchwingRecipes/StandardArchwing/StandardArchwingSystemsBlueprint",
 | 
			
		||||
                            ItemCount: -1
 | 
			
		||||
                        }
 | 
			
		||||
                    ]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Keys/ModQuest/ModQuestKeyChain": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Recipes/Components/VorBoltRemoverBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/ChromaBlueprint": {
 | 
			
		||||
            await addItems(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconABlueprint",
 | 
			
		||||
                    ItemCount: -1
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconBBlueprint",
 | 
			
		||||
                    ItemCount: -1
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Recipes/WarframeRecipes/ChromaBeaconCBlueprint",
 | 
			
		||||
                    ItemCount: -1
 | 
			
		||||
                }
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/OctaviaBlueprint": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Keys/BardQuest/BardQuestSequencerBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, -1);
 | 
			
		||||
                    await addItems(inventory, [
 | 
			
		||||
                        {
 | 
			
		||||
                            ItemType: "/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartA",
 | 
			
		||||
                            ItemCount: -1
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            ItemType: "/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartB",
 | 
			
		||||
                            ItemCount: -1
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            ItemType: "/Lotus/Types/Keys/BardQuest/BardQuestSequencerPartC",
 | 
			
		||||
                            ItemCount: -1
 | 
			
		||||
                        }
 | 
			
		||||
                    ]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Game/CrewShip/Ships/RailJack": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Recipes/Railjack/RailjackCephalonBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, recipeItem.ItemCount * -1);
 | 
			
		||||
                    await unlockShipFeature(inventory, recipe.resultType);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Items/ShipDecos/MummyQuestVessel": {
 | 
			
		||||
            const gearItem = inventory.Consumables.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Keys/MummyQuest/MummyArtifact01GearItem"
 | 
			
		||||
            );
 | 
			
		||||
            if (gearItem && gearItem.ItemCount > 0) {
 | 
			
		||||
                await addItem(inventory, gearItem.ItemType, gearItem.ItemCount * -1);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Types/Recipes/WarframeRecipes/ConcreteFrameBlueprint": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Gameplay/EntratiLab/Quest/GargoyleRecipeItem"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    await addItem(inventory, recipe.resultType);
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, recipeItem.ItemCount * -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        case "/Lotus/Upgrades/Skins/Umbra/UmbraAltHelmet": {
 | 
			
		||||
            const recipeItem = inventory.Recipes.find(
 | 
			
		||||
                i => i.ItemType == "/Lotus/Types/Recipes/WarframeRecipes/ExcaliburUmbraBlueprint"
 | 
			
		||||
            );
 | 
			
		||||
            if (recipeItem && recipeItem.ItemCount > 0) {
 | 
			
		||||
                const recipe = ExportRecipes[recipeItem.ItemType];
 | 
			
		||||
                if (!inventory.MiscItems.find(i => i.ItemType == recipe.resultType)) {
 | 
			
		||||
                    const umbraModA = (
 | 
			
		||||
                        await addItem(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
 | 
			
		||||
                            1,
 | 
			
		||||
                            false,
 | 
			
		||||
                            undefined,
 | 
			
		||||
                            `{"lvl":5}`
 | 
			
		||||
                        )
 | 
			
		||||
                    ).Upgrades![0];
 | 
			
		||||
                    const umbraModB = (
 | 
			
		||||
                        await addItem(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
 | 
			
		||||
                            1,
 | 
			
		||||
                            false,
 | 
			
		||||
                            undefined,
 | 
			
		||||
                            `{"lvl":5}`
 | 
			
		||||
                        )
 | 
			
		||||
                    ).Upgrades![0];
 | 
			
		||||
                    const umbraModC = (
 | 
			
		||||
                        await addItem(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
 | 
			
		||||
                            1,
 | 
			
		||||
                            false,
 | 
			
		||||
                            undefined,
 | 
			
		||||
                            `{"lvl":5}`
 | 
			
		||||
                        )
 | 
			
		||||
                    ).Upgrades![0];
 | 
			
		||||
                    const sacrificeModA = (
 | 
			
		||||
                        await addItem(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
 | 
			
		||||
                            1,
 | 
			
		||||
                            false,
 | 
			
		||||
                            undefined,
 | 
			
		||||
                            `{"lvl":5}`
 | 
			
		||||
                        )
 | 
			
		||||
                    ).Upgrades![0];
 | 
			
		||||
                    const sacrificeModB = (
 | 
			
		||||
                        await addItem(
 | 
			
		||||
                            inventory,
 | 
			
		||||
                            "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
 | 
			
		||||
                            1,
 | 
			
		||||
                            false,
 | 
			
		||||
                            undefined,
 | 
			
		||||
                            `{"lvl":5}`
 | 
			
		||||
                        )
 | 
			
		||||
                    ).Upgrades![0];
 | 
			
		||||
 | 
			
		||||
                    await addPowerSuit(inventory, "/Lotus/Powersuits/Excalibur/ExcaliburUmbra", {
 | 
			
		||||
                        Configs: [
 | 
			
		||||
                            {
 | 
			
		||||
                                Upgrades: [
 | 
			
		||||
                                    "",
 | 
			
		||||
                                    "",
 | 
			
		||||
                                    "",
 | 
			
		||||
                                    "",
 | 
			
		||||
                                    "",
 | 
			
		||||
                                    umbraModA.ItemId.$oid,
 | 
			
		||||
                                    umbraModB.ItemId.$oid,
 | 
			
		||||
                                    umbraModC.ItemId.$oid
 | 
			
		||||
                                ]
 | 
			
		||||
                            }
 | 
			
		||||
                        ],
 | 
			
		||||
                        XP: 900_000,
 | 
			
		||||
                        Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
			
		||||
                    });
 | 
			
		||||
                    inventory.XPInfo.push({
 | 
			
		||||
                        ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
			
		||||
                        XP: 900_000
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    addEquipment(inventory, "Melee", "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana", {
 | 
			
		||||
                        Configs: [
 | 
			
		||||
                            {
 | 
			
		||||
                                Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid]
 | 
			
		||||
                            }
 | 
			
		||||
                        ],
 | 
			
		||||
                        XP: 450_000,
 | 
			
		||||
                        Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
			
		||||
                    });
 | 
			
		||||
                    inventory.XPInfo.push({
 | 
			
		||||
                        ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
			
		||||
                        XP: 450_000
 | 
			
		||||
                    });
 | 
			
		||||
                    if (recipe.consumeOnUse) await addItem(inventory, recipeItem.ItemType, recipeItem.ItemCount * -1);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        default:
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -12,8 +12,8 @@ let httpServer: http.Server | undefined;
 | 
			
		||||
let httpsServer: https.Server | undefined;
 | 
			
		||||
 | 
			
		||||
const tlsOptions = {
 | 
			
		||||
    key: fs.readFileSync("static/cert/key.pem"),
 | 
			
		||||
    cert: fs.readFileSync("static/cert/cert.pem")
 | 
			
		||||
    key: fs.readFileSync("static/certs/key.pem"),
 | 
			
		||||
    cert: fs.readFileSync("static/certs/cert.pem")
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const startWebServer = (): void => {
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import invasionNodes from "../../static/fixed_responses/worldState/invasionNodes
 | 
			
		||||
import invasionRewards from "../../static/fixed_responses/worldState/invasionRewards.json" with { type: "json" };
 | 
			
		||||
import pvpChallenges from "../../static/fixed_responses/worldState/pvpChallenges.json" with { type: "json" };
 | 
			
		||||
import { buildConfig } from "./buildConfigService.ts";
 | 
			
		||||
import { EPOCH, unixTimesInMs } from "../constants/timeConstants.ts";
 | 
			
		||||
import { unixTimesInMs } from "../constants/timeConstants.ts";
 | 
			
		||||
import { config } from "./configService.ts";
 | 
			
		||||
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "./rngService.ts";
 | 
			
		||||
import type { IMissionReward, IRegion, TFaction } from "warframe-public-export-plus";
 | 
			
		||||
@ -41,7 +41,6 @@ import type {
 | 
			
		||||
import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { logger } from "../utils/logger.ts";
 | 
			
		||||
import { DailyDeal, Fissure } from "../models/worldStateModel.ts";
 | 
			
		||||
import { getConquest } from "./conquestService.ts";
 | 
			
		||||
 | 
			
		||||
const sortieBosses = [
 | 
			
		||||
    "SORTIE_BOSS_HYENA",
 | 
			
		||||
@ -277,6 +276,8 @@ const microplanetEndlessJobs: readonly string[] = [
 | 
			
		||||
    "/Lotus/Types/Gameplay/InfestedMicroplanet/Jobs/DeimosEndlessPurifyBounty"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
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 => {
 | 
			
		||||
    return nowMs + 300_000 > thenMs;
 | 
			
		||||
};
 | 
			
		||||
@ -3181,7 +3182,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
    // Nightwave Challenges
 | 
			
		||||
    const nightwaveSyndicateTag = getNightwaveSyndicateTag(buildLabel);
 | 
			
		||||
    if (nightwaveSyndicateTag) {
 | 
			
		||||
        const nightwaveStartTimestamp = nightwaveTagToActivation[nightwaveSyndicateTag] ?? 1747851300000;
 | 
			
		||||
        const nightwaveStartTimestamp = 1747851300000;
 | 
			
		||||
        const nightwaveSeason = nightwaveTagToSeason[nightwaveSyndicateTag];
 | 
			
		||||
        worldState.SeasonInfo = {
 | 
			
		||||
            Activation: { $date: { $numberLong: nightwaveStartTimestamp.toString() } },
 | 
			
		||||
@ -3468,18 +3469,6 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Void Storms
 | 
			
		||||
    const hour = Math.trunc(timeMs / unixTimesInMs.hour);
 | 
			
		||||
    const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
 | 
			
		||||
    const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
 | 
			
		||||
    if (overLastHourStormExpiry > timeMs) {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour - 2);
 | 
			
		||||
    }
 | 
			
		||||
    pushVoidStorms(worldState.VoidStorms, hour - 1);
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sortie & syndicate missions cycling every day (at 16:00 or 17:00 UTC depending on if London, OT is observing DST)
 | 
			
		||||
    {
 | 
			
		||||
        const rollover = getSortieTime(day);
 | 
			
		||||
@ -3562,18 +3551,16 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!buildLabel || version_compare(buildLabel, "2025.10.14.16.10") >= 0) {
 | 
			
		||||
        worldState.Conquests = [];
 | 
			
		||||
        {
 | 
			
		||||
            const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4];
 | 
			
		||||
            worldState.Conquests.push(getConquest("CT_LAB", week, null));
 | 
			
		||||
            worldState.Conquests.push(getConquest("CT_HEX", week, season));
 | 
			
		||||
        }
 | 
			
		||||
        if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) {
 | 
			
		||||
            const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[(week + 1) % 4];
 | 
			
		||||
            worldState.Conquests.push(getConquest("CT_LAB", week, null));
 | 
			
		||||
            worldState.Conquests.push(getConquest("CT_HEX", week, season));
 | 
			
		||||
        }
 | 
			
		||||
    // Void Storms
 | 
			
		||||
    const hour = Math.trunc(timeMs / unixTimesInMs.hour);
 | 
			
		||||
    const overLastHourStormExpiry = hour * unixTimesInMs.hour + 10 * unixTimesInMs.minute;
 | 
			
		||||
    const thisHourStormActivation = hour * unixTimesInMs.hour + 40 * unixTimesInMs.minute;
 | 
			
		||||
    if (overLastHourStormExpiry > timeMs) {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour - 2);
 | 
			
		||||
    }
 | 
			
		||||
    pushVoidStorms(worldState.VoidStorms, hour - 1);
 | 
			
		||||
    if (isBeforeNextExpectedWorldStateRefresh(timeMs, thisHourStormActivation)) {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sentient Anomaly + Xtra Cheese cycles
 | 
			
		||||
@ -3790,10 +3777,7 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string
 | 
			
		||||
            valid_values: Object.keys(nightwaveTagToSeason)
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    if (!buildLabel || version_compare(buildLabel, "2025.10.14.16.10") >= 0) {
 | 
			
		||||
        return "RadioLegionIntermission14Syndicate";
 | 
			
		||||
    }
 | 
			
		||||
    if (version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
 | 
			
		||||
    if (!buildLabel || version_compare(buildLabel, "2025.05.20.10.18") >= 0) {
 | 
			
		||||
        return "RadioLegionIntermission13Syndicate";
 | 
			
		||||
    }
 | 
			
		||||
    if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
 | 
			
		||||
@ -3803,7 +3787,6 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const nightwaveTagToSeason: Record<string, number> = {
 | 
			
		||||
    RadioLegionIntermission14Syndicate: 16, // Nora's Mix: Dreams of the Dead
 | 
			
		||||
    RadioLegionIntermission13Syndicate: 15, // Nora's Mix Vol. 9
 | 
			
		||||
    RadioLegionIntermission12Syndicate: 14, // Nora's Mix Vol. 8
 | 
			
		||||
    RadioLegionIntermission11Syndicate: 13, // Nora's Mix Vol. 7
 | 
			
		||||
@ -3822,10 +3805,6 @@ const nightwaveTagToSeason: Record<string, number> = {
 | 
			
		||||
    RadioLegionSyndicate: 0 // The Wolf of Saturn Six
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const nightwaveTagToActivation: Record<string, number> = {
 | 
			
		||||
    RadioLegionIntermission14Syndicate: 1761589199000
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const updateFissures = async (): Promise<void> => {
 | 
			
		||||
    const fissures = await Fissure.find();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,6 @@ export enum EquipmentFeatures {
 | 
			
		||||
    GRAVIMAG_INSTALLED = 4,
 | 
			
		||||
    GILDED = 8,
 | 
			
		||||
    ARCANE_SLOT = 32,
 | 
			
		||||
    SECOND_ARCANE_SLOT = 64,
 | 
			
		||||
    INCARNON_GENESIS = 512,
 | 
			
		||||
    VALENCE_SWAP = 1024
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ import type { IOrbiterClient } from "../personalRoomsTypes.ts";
 | 
			
		||||
import type { ICountedStoreItem } from "warframe-public-export-plus";
 | 
			
		||||
import type { IEquipmentClient, IEquipmentDatabase, ITraits } from "../equipmentTypes.ts";
 | 
			
		||||
import type { ILoadOutPresets } from "../saveLoadoutTypes.ts";
 | 
			
		||||
import type { CalendarSeasonType } from "../worldStateTypes.ts";
 | 
			
		||||
 | 
			
		||||
export type InventoryDatabaseEquipment = {
 | 
			
		||||
    [_ in TEquipmentKey]: IEquipmentDatabase[];
 | 
			
		||||
@ -437,7 +436,6 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
 | 
			
		||||
    Ship?: IOrbiterClient; // U22 and below, response only
 | 
			
		||||
    ClaimedJunctionChallengeRewards?: string[]; // U39
 | 
			
		||||
    SpecialItemRewardAttenuation?: IRewardAttenuation[]; // Baro's Void Surplus
 | 
			
		||||
    NokkoColony?: INokkoColony; // Field Guide
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IAffiliation {
 | 
			
		||||
@ -1181,7 +1179,7 @@ export interface IMarker {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ISeasonProgress {
 | 
			
		||||
    SeasonType: CalendarSeasonType;
 | 
			
		||||
    SeasonType: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
 | 
			
		||||
    LastCompletedDayIdx: number;
 | 
			
		||||
    LastCompletedChallengeDayIdx: number;
 | 
			
		||||
    ActivatedChallenges: string[];
 | 
			
		||||
@ -1222,13 +1220,3 @@ export interface IHubNpcCustomization {
 | 
			
		||||
    Pattern: string;
 | 
			
		||||
    Tag: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IJournalEntry {
 | 
			
		||||
    EntryType: string;
 | 
			
		||||
    Progress: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface INokkoColony {
 | 
			
		||||
    FeedLevel: number;
 | 
			
		||||
    JournalEntries: IJournalEntry[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -55,8 +55,6 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons {
 | 
			
		||||
    DTLS?: number;
 | 
			
		||||
    IRC?: string[];
 | 
			
		||||
    HUB?: string;
 | 
			
		||||
    NatHash?: string;
 | 
			
		||||
    SteamId?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IGroup {
 | 
			
		||||
 | 
			
		||||
@ -32,7 +32,6 @@ export interface IWorldState {
 | 
			
		||||
        ActiveChallenges: ISeasonChallenge[];
 | 
			
		||||
    };
 | 
			
		||||
    KnownCalendarSeasons: ICalendarSeason[];
 | 
			
		||||
    Conquests?: IConquest[];
 | 
			
		||||
    Tmp?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -353,11 +352,10 @@ export interface ISeasonChallenge {
 | 
			
		||||
    Challenge: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CalendarSeasonType = "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
 | 
			
		||||
export interface ICalendarSeason {
 | 
			
		||||
    Activation: IMongoDate;
 | 
			
		||||
    Expiry: IMongoDate;
 | 
			
		||||
    Season: CalendarSeasonType;
 | 
			
		||||
    Season: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
 | 
			
		||||
    Days: ICalendarDay[];
 | 
			
		||||
    YearIteration: number;
 | 
			
		||||
    Version: number;
 | 
			
		||||
@ -418,33 +416,6 @@ export interface IGameMarketCategory {
 | 
			
		||||
    Items?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// >= 40.0.0
 | 
			
		||||
export type TConquestType = "CT_LAB" | "CT_HEX";
 | 
			
		||||
export interface IConquest {
 | 
			
		||||
    Activation: IMongoDate;
 | 
			
		||||
    Expiry: IMongoDate;
 | 
			
		||||
    Type: TConquestType;
 | 
			
		||||
    Missions: IConquestMission[];
 | 
			
		||||
    Variables: [string, string, string, string];
 | 
			
		||||
    RandomSeed: number;
 | 
			
		||||
}
 | 
			
		||||
export interface IConquestMission {
 | 
			
		||||
    faction: TFaction;
 | 
			
		||||
    missionType: TMissionType;
 | 
			
		||||
    difficulties: [
 | 
			
		||||
        {
 | 
			
		||||
            type: "CD_NORMAL";
 | 
			
		||||
            deviation: string;
 | 
			
		||||
            risks: [string];
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            type: "CD_HARD";
 | 
			
		||||
            deviation: string;
 | 
			
		||||
            risks: [string, string];
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITmp {
 | 
			
		||||
    cavabegin: string;
 | 
			
		||||
    PurchasePlatformLockEnabled: boolean; // Seems unused
 | 
			
		||||
@ -452,8 +423,6 @@ export interface ITmp {
 | 
			
		||||
    ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0)
 | 
			
		||||
    mbrt?: boolean; // Related to mobile app rating request
 | 
			
		||||
    fbst: IFbst;
 | 
			
		||||
    lqo?: IConquestOverride;
 | 
			
		||||
    hqo?: IConquestOverride;
 | 
			
		||||
    sfn: number;
 | 
			
		||||
    edg?: TCircuitGameMode[]; // The Circuit game modes overwrite
 | 
			
		||||
}
 | 
			
		||||
@ -482,12 +451,3 @@ interface IFbst {
 | 
			
		||||
    e: number;
 | 
			
		||||
    n: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// < 40.0.0
 | 
			
		||||
interface IConquestOverride {
 | 
			
		||||
    mt?: string[]; // mission types but "Exterminate" instead of "MT_EXTERMINATION", etc. and "DualDefense" instead of "Defense" for hex conquest
 | 
			
		||||
    mv?: string[];
 | 
			
		||||
    mf?: number[]; // hex conquest only
 | 
			
		||||
    c?: [string, string][];
 | 
			
		||||
    fv?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,70 +0,0 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIF/DCCBGSgAwIBAgIQH1nGumKdC869SnXDdjpRizANBgkqhkiG9w0BAQsFADBg
 | 
			
		||||
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD
 | 
			
		||||
Ey5TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2
 | 
			
		||||
MB4XDTI1MTAzMTAwMDAwMFoXDTI2MDMwNjIzNTk1OVowGDEWMBQGA1UEAwwNKi5m
 | 
			
		||||
YWtldGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKZt+8pe
 | 
			
		||||
ZABBMSORWDlJBKykSbbNeEX2vPNrsQBqa3aBzQLdp6dm8qw099a5mn/vuQzdtFhc
 | 
			
		||||
QbaemxoVViszzM3dpTlOgLyl2r4cr8KMXJ/rRMqLeaYks+BVYJyxKAlVIduJCpP+
 | 
			
		||||
cxYUIiz+zNGMucuFdanzeNfCRhakDkzHH/LKZFYDHeqdR8vdUnOH8n9xuGR7sC6I
 | 
			
		||||
PbfML8SXCnazMpcPAeIFzJeZW3fbFIQ5R6B7X38k9+wUzKXrJo0+Q8U1mIAsV7fh
 | 
			
		||||
4gn+xgrFOJ7T2Vd8EtHgnr5nzNRDk4prE+ecTxBveL4QGuNoPaABtor6OLBR54AQ
 | 
			
		||||
8ycj29lQ094BrJcCAwEAAaOCAngwggJ0MB8GA1UdIwQYMBaAFGjAEhYYDq/O9oem
 | 
			
		||||
MlejRlFdywcnMB0GA1UdDgQWBBRqYi9dBjbiV9wE6GEtzyzUM6cQmjAOBgNVHQ8B
 | 
			
		||||
Af8EBAMCBaAwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDATBJBgNV
 | 
			
		||||
HSAEQjBAMDQGCysGAQQBsjEBAgIHMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8vc2Vj
 | 
			
		||||
dGlnby5jb20vQ1BTMAgGBmeBDAECATCBhAYIKwYBBQUHAQEEeDB2ME8GCCsGAQUF
 | 
			
		||||
BzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNTZXJ2ZXJB
 | 
			
		||||
dXRoZW50aWNhdGlvbkNBRFZSMzYuY3J0MCMGCCsGAQUFBzABhhdodHRwOi8vb2Nz
 | 
			
		||||
cC5zZWN0aWdvLmNvbTAlBgNVHREEHjAcgg0qLmZha2V0bHMuY29tggtmYWtldGxz
 | 
			
		||||
LmNvbTCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB2AJaXZL9VWJet90OHaDcIQnfp
 | 
			
		||||
8DrV9qTzNm5GpD8PyqnGAAABmjqerq4AAAQDAEcwRQIgY1YdLRDkbq9U9qGLpX5M
 | 
			
		||||
IESNpCLFKVgoPLyNQQejtPUCIQCGlFB13fGO7GpVZLXyuOrXIWRZQUxnm1GqhvOP
 | 
			
		||||
BoovPgB2ANFuqaVoB35mNaA/N6XdvAOlPEESFNSIGPXpMbMjy5UEAAABmjqerwkA
 | 
			
		||||
AAQDAEcwRQIhAMxyLWnCpof354KMPYiWQmqd+2D+yyV5ZL7rmvIJw0ojAiAxxsid
 | 
			
		||||
se3hTmdo39MJLKyVqlZKUua5dmLckUYUImsogTANBgkqhkiG9w0BAQsFAAOCAYEA
 | 
			
		||||
ByJlO00LMIgL6d5xfouZHl1OL2w0DDiWHQa87BM9AqUFSgE1IsWyUAMTVoxNp3bM
 | 
			
		||||
c4fiBoJ9P32hDft2gJRCtJ+xOlU9ufQqCfpN9pBxe8eEldJdnHi7q9Y1dIVwRA+y
 | 
			
		||||
xY2r1xgTmjYgv1Uo5mQoOjgvrRdUNTyW0Rkp9+9zr6NFxi4orqypt4KKOFY2DlV9
 | 
			
		||||
wKmS4HPXSNR6QEylUR1+IsCP7iQzgTcZDYYLH2vAekJ1vFJkxZyB8a1vjuUxohPi
 | 
			
		||||
JoXnOqBu5T9JdE81/Rm01bHQ6XAiAHpQRCyZZ3TBUgpPd10DrV8K/gMuBduKOkgk
 | 
			
		||||
BnwNZjOCcCwgI66F5kmoeT95ovn8ApKO9aT3EfYXx3XM+xaWjEQRpXulB9AYjQvg
 | 
			
		||||
wxdW+cpHAa/ttPEnrb4y2Az4uYMM2016U5p1o6SP3/feGbWSsJ33OF4VvSaS1x0h
 | 
			
		||||
DgzfqIX4/8yeYsfNNsoSpcwV9UGBZ7Zs0Avy1Mm5ah0Z6CGGKVWb7Qqse1YR9MSa
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIGTDCCBDSgAwIBAgIQOXpmzCdWNi4NqofKbqvjsTANBgkqhkiG9w0BAQwFADBf
 | 
			
		||||
MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD
 | 
			
		||||
Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw
 | 
			
		||||
HhcNMjEwMzIyMDAwMDAwWhcNMzYwMzIxMjM1OTU5WjBgMQswCQYDVQQGEwJHQjEY
 | 
			
		||||
MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFB1Ymxp
 | 
			
		||||
YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gQ0EgRFYgUjM2MIIBojANBgkqhkiG9w0B
 | 
			
		||||
AQEFAAOCAY8AMIIBigKCAYEAljZf2HIz7+SPUPQCQObZYcrxLTHYdf1ZtMRe7Yeq
 | 
			
		||||
RPSwygz16qJ9cAWtWNTcuICc++p8Dct7zNGxCpqmEtqifO7NvuB5dEVexXn9RFFH
 | 
			
		||||
12Hm+NtPRQgXIFjx6MSJcNWuVO3XGE57L1mHlcQYj+g4hny90aFh2SCZCDEVkAja
 | 
			
		||||
EMMfYPKuCjHuuF+bzHFb/9gV8P9+ekcHENF2nR1efGWSKwnfG5RawlkaQDpRtZTm
 | 
			
		||||
M64TIsv/r7cyFO4nSjs1jLdXYdz5q3a4L0NoabZfbdxVb+CUEHfB0bpulZQtH1Rv
 | 
			
		||||
38e/lIdP7OTTIlZh6OYL6NhxP8So0/sht/4J9mqIGxRFc0/pC8suja+wcIUna0HB
 | 
			
		||||
pXKfXTKpzgis+zmXDL06ASJf5E4A2/m+Hp6b84sfPAwQ766rI65mh50S0Di9E3Pn
 | 
			
		||||
2WcaJc+PILsBmYpgtmgWTR9eV9otfKRUBfzHUHcVgarub/XluEpRlTtZudU5xbFN
 | 
			
		||||
xx/DgMrXLUAPaI60fZ6wA+PTAgMBAAGjggGBMIIBfTAfBgNVHSMEGDAWgBRWc1hk
 | 
			
		||||
lfmSGrASKgRieaFAFYghSTAdBgNVHQ4EFgQUaMASFhgOr872h6YyV6NGUV3LBycw
 | 
			
		||||
DgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0lBBYwFAYI
 | 
			
		||||
KwYBBQUHAwEGCCsGAQUFBwMCMBsGA1UdIAQUMBIwBgYEVR0gADAIBgZngQwBAgEw
 | 
			
		||||
VAYDVR0fBE0wSzBJoEegRYZDaHR0cDovL2NybC5zZWN0aWdvLmNvbS9TZWN0aWdv
 | 
			
		||||
UHVibGljU2VydmVyQXV0aGVudGljYXRpb25Sb290UjQ2LmNybDCBhAYIKwYBBQUH
 | 
			
		||||
AQEEeDB2ME8GCCsGAQUFBzAChkNodHRwOi8vY3J0LnNlY3RpZ28uY29tL1NlY3Rp
 | 
			
		||||
Z29QdWJsaWNTZXJ2ZXJBdXRoZW50aWNhdGlvblJvb3RSNDYucDdjMCMGCCsGAQUF
 | 
			
		||||
BzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQwFAAOCAgEA
 | 
			
		||||
YtOC9Fy+TqECFw40IospI92kLGgoSZGPOSQXMBqmsGWZUQ7rux7cj1du6d9rD6C8
 | 
			
		||||
ze1B2eQjkrGkIL/OF1s7vSmgYVafsRoZd/IHUrkoQvX8FZwUsmPu7amgBfaY3g+d
 | 
			
		||||
q1x0jNGKb6I6Bzdl6LgMD9qxp+3i7GQOnd9J8LFSietY6Z4jUBzVoOoz8iAU84OF
 | 
			
		||||
h2HhAuiPw1ai0VnY38RTI+8kepGWVfGxfBWzwH9uIjeooIeaosVFvE8cmYUB4TSH
 | 
			
		||||
5dUyD0jHct2+8ceKEtIoFU/FfHq/mDaVnvcDCZXtIgitdMFQdMZaVehmObyhRdDD
 | 
			
		||||
4NQCs0gaI9AAgFj4L9QtkARzhQLNyRf87Kln+YU0lgCGr9HLg3rGO8q+Y4ppLsOd
 | 
			
		||||
unQZ6ZxPNGIfOApbPVf5hCe58EZwiWdHIMn9lPP6+F404y8NNugbQixBber+x536
 | 
			
		||||
WrZhFZLjEkhp7fFXf9r32rNPfb74X/U90Bdy4lzp3+X1ukh1BuMxA/EEhDoTOS3l
 | 
			
		||||
7ABvc7BYSQubQ2490OcdkIzUh3ZwDrakMVrbaTxUM2p24N6dB+ns2zptWCva6jzW
 | 
			
		||||
r8IWKIMxzxLPv5Kt3ePKcUdvkBU/smqujSczTzzSjIoR5QqQA6lN1ZRSnuHIWCvh
 | 
			
		||||
JEltkYnTAH41QJ6SAWO66GrrUESwN/cgZzL4JLEqz1Y=
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
@ -1,28 +0,0 @@
 | 
			
		||||
-----BEGIN PRIVATE KEY-----
 | 
			
		||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCmbfvKXmQAQTEj
 | 
			
		||||
kVg5SQSspEm2zXhF9rzza7EAamt2gc0C3aenZvKsNPfWuZp/77kM3bRYXEG2npsa
 | 
			
		||||
FVYrM8zN3aU5ToC8pdq+HK/CjFyf60TKi3mmJLPgVWCcsSgJVSHbiQqT/nMWFCIs
 | 
			
		||||
/szRjLnLhXWp83jXwkYWpA5Mxx/yymRWAx3qnUfL3VJzh/J/cbhke7AuiD23zC/E
 | 
			
		||||
lwp2szKXDwHiBcyXmVt32xSEOUege19/JPfsFMyl6yaNPkPFNZiALFe34eIJ/sYK
 | 
			
		||||
xTie09lXfBLR4J6+Z8zUQ5OKaxPnnE8Qb3i+EBrjaD2gAbaK+jiwUeeAEPMnI9vZ
 | 
			
		||||
UNPeAayXAgMBAAECggEAOYwMJVRwFZp1KExIij5STHPePURc0yxW94CESpWBpQ+K
 | 
			
		||||
2PPV1c+GF7+U9v1ki9pTTTyX8HmuCzxaezFngza9GW4LhH49i3153oTCzW2FVZKf
 | 
			
		||||
Tb3eiXFldStwZZ3oLxntxCBltPilyLubeZ19KvQTBmmWXvaeEVTOsWN2wluUE3oT
 | 
			
		||||
QEEFJmvWSj6Ow5HwA2xZP5dD4gv7nCY1swTePVLgWtJDC3AWDmKv3mB28brmtaDw
 | 
			
		||||
wP/Oq9LLlHJDK+AzJEuUs+VDFQi7j6yqXWglSjO3Jwkkfkqg/SSoZ9BKdd6i0DLs
 | 
			
		||||
UnUWHHATpv0Lwsa7w8csQBwfskUxsXECZpwzpCgsIQKBgQDYl2wvwjjG6qZtr060
 | 
			
		||||
rj4PDkFLxFbtlmBiyW9ShM6iqK6FWQJJ1FNr6MdM2zBqw+n4tldpU3DnuZQphgZK
 | 
			
		||||
57jejWFsHH5DYkS8k0k021AYT2IyCFHoEF43LlbRDXQguw1fADnoy6FpJBaRzuxx
 | 
			
		||||
jPEHdB/aYqngmTXglFYkizN2BwKBgQDEthR1FV5iPdQXKSR+Yu2irBzpxY1xhdd2
 | 
			
		||||
V5082etd2vBvAT7e4k8MroNMuYqL8miu2SeTJEyNkpCVoxQPF4uavandoRuEFIZl
 | 
			
		||||
8VPVvG5xqYIqXAJ4XlSImEgUww+jJfyLzHT0TZi6rrdHV/8zUXEimjc6OBZVafDg
 | 
			
		||||
FP6Lqo/w8QKBgQCxx3B8rv3tgDNFOqzur0qvDvNXnnv/nfvVeiPO5sW5S52cRJgV
 | 
			
		||||
Q5uJqlLUaeGO8Oo+RGTxRhUZjwDnKGRH3XWn7wI1PBoDc0iaRIbFRPK0UYx3Js8c
 | 
			
		||||
HTtILdgC1fko2IA8JzJhO6tsYrvHyMHY3mgExzNSDMQFX5ySjw86BawixwKBgBGc
 | 
			
		||||
J0qwBgoPdOw53618179HXzNCXz45eCd9AnOPIrX9Qqb9Wo6DfgYpnVGCDrgmlF6K
 | 
			
		||||
zDMs/bly1ITA26vaNMI+lnVj1d3GJJ39s76fpteAEEoQgJwb/b9YuqM5Ly4w2WH+
 | 
			
		||||
hL3WMIUN3RSC+TKz6MfrPGR23vD4kfrNhlgkhcxRAoGAEb6nXwyWe5hg9+qE2fcf
 | 
			
		||||
W27VApoalmnDOGFHtGhUitbBw7mHPb6wsSh4mx5Ucccfb5WUO3Cq5aW3qsDdkM+a
 | 
			
		||||
YrBDbvw7PPcAX0vokpyFQvHH35AsIXQuYlQyGzcCdZkpFPIB5gVbLvbgxpBvEbVZ
 | 
			
		||||
mGJpw7X/6Pg0ux0Ze11cr2c=
 | 
			
		||||
-----END PRIVATE KEY-----
 | 
			
		||||
							
								
								
									
										71
									
								
								static/certs/cert.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								static/certs/cert.pem
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,71 @@
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIGMDCCBRigAwIBAgIQX4800cgswlDH/QexMSnnnjANBgkqhkiG9w0BAQsFADCB
 | 
			
		||||
jzELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
 | 
			
		||||
A1UEBxMHU2FsZm9yZDEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQD
 | 
			
		||||
Ey5TZWN0aWdvIFJTQSBEb21haW4gVmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENB
 | 
			
		||||
MB4XDTI1MDMwNjAwMDAwMFoXDTI2MDMwNjIzNTk1OVowGDEWMBQGA1UEAwwNKi5m
 | 
			
		||||
YWtldGxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMe42XWK
 | 
			
		||||
HJuR7doFTX79zrEKfTlD2hjRIif3dHKJNTJNvZa52mIoHelP7RVUuFOhp7aZCNLh
 | 
			
		||||
IEzDyZObl8vwO6L2PVu5tbBEEoNixbpfhc8ZICEBuVo2UAhnJFcMJtuvtrCq+7ye
 | 
			
		||||
oczM/k/nh8FBz2WnLzWs4CZt1sa5knZXFmBmsHJQtQIC6vx7QzVcKGOlAosIEHSK
 | 
			
		||||
X4nIz5fLgWSzor1Gay56j31PTk+qRvlPQM2aKiLWnlLfRED4zHJqLe94itu8llPX
 | 
			
		||||
b6g+cLxxRKUpMqtG/15cDdBZwv40Dja7bmNfe1u4w2QCVLjvHVaVpNXbcRay/Mhn
 | 
			
		||||
M1w5LzDZmV58b18CAwEAAaOCAvwwggL4MB8GA1UdIwQYMBaAFI2MXsRUrYrhd+mb
 | 
			
		||||
+ZsF4bgBjWHhMB0GA1UdDgQWBBS6/x/N38wMJrQq/cE1oIcRERMonTAOBgNVHQ8B
 | 
			
		||||
Af8EBAMCBaAwDAYDVR0TAQH/BAIwADAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
 | 
			
		||||
BQUHAwIwSQYDVR0gBEIwQDA0BgsrBgEEAbIxAQICBzAlMCMGCCsGAQUFBwIBFhdo
 | 
			
		||||
dHRwczovL3NlY3RpZ28uY29tL0NQUzAIBgZngQwBAgEwgYQGCCsGAQUFBwEBBHgw
 | 
			
		||||
djBPBggrBgEFBQcwAoZDaHR0cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUlNB
 | 
			
		||||
RG9tYWluVmFsaWRhdGlvblNlY3VyZVNlcnZlckNBLmNydDAjBggrBgEFBQcwAYYX
 | 
			
		||||
aHR0cDovL29jc3Auc2VjdGlnby5jb20wJQYDVR0RBB4wHIINKi5mYWtldGxzLmNv
 | 
			
		||||
bYILZmFrZXRscy5jb20wggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2AJaXZL9V
 | 
			
		||||
WJet90OHaDcIQnfp8DrV9qTzNm5GpD8PyqnGAAABlWsz5fgAAAQDAEcwRQIgTN7Y
 | 
			
		||||
/mDqiD3RbGVLEOQK2wvXsboBolBRwGJFuFEsDScCIQCQ0qfb/0V8qqSxrkx/PiVS
 | 
			
		||||
1lSn5gBEnQUiQOkefcnW0gB2ABmG1Mcoqm/+ugNveCpNAZGqzi1yMQ+uzl1wQS0l
 | 
			
		||||
TMfUAAABlWsz5dAAAAQDAEcwRQIhAJnQJyrSCWWdi9Kyoa7XuMGyDKt183jJMY0E
 | 
			
		||||
71abTuBOAiBC+WnK1esG6xr8aVGHRcc+1U/I7LiaG3LCRMYtCKrTGwB2AMs49xWJ
 | 
			
		||||
fIShRF9bwd37yW7ymlnNRwppBYWwyxTDFFjnAAABlWsz5f4AAAQDAEcwRQIhAJUs
 | 
			
		||||
4PWDwyQJnCxCyEwFlFUY2uYQkGrQPA9f9Sw5Xk1fAiB63eQtZQGjvzvhOghy6z9a
 | 
			
		||||
8oGYbDfDQ/zfisMYO7rM6zANBgkqhkiG9w0BAQsFAAOCAQEAEHnSoeBbWiK3CS3a
 | 
			
		||||
px0BL+YXxRxdUcTMHgn5o+LlI9sWlpf+JLXmn7Z4QA6fAwT4k/Ue7xsmIq0OraDk
 | 
			
		||||
/pEVXWm1HO/9wUkGQg0DBi77BpfHircd7OWIMdt250Q8UAmZkOyhVgnwBcScqMwq
 | 
			
		||||
2T5CPaYvYGgYWx/qkIBv7JqhVbrP82rnF9b9ZUZ8GIE31chBmtMva9AsnAN5dmRw
 | 
			
		||||
81bVvPWXUfX30CYu5sxeWL06Zpy9nfJumxZri1SWXNTBjSvud2jsZ8tSCUAWLL/4
 | 
			
		||||
ui3Vien9m2oMOpaA8xbS88ZTk9Alm/o5febEKJZUPlytQzij8gQpiovFw2v+Cdei
 | 
			
		||||
+tFXKw==
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
-----BEGIN CERTIFICATE-----
 | 
			
		||||
MIIGEzCCA/ugAwIBAgIQfVtRJrR2uhHbdBYLvFMNpzANBgkqhkiG9w0BAQwFADCB
 | 
			
		||||
iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl
 | 
			
		||||
cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV
 | 
			
		||||
BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTgx
 | 
			
		||||
MTAyMDAwMDAwWhcNMzAxMjMxMjM1OTU5WjCBjzELMAkGA1UEBhMCR0IxGzAZBgNV
 | 
			
		||||
BAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEYMBYGA1UE
 | 
			
		||||
ChMPU2VjdGlnbyBMaW1pdGVkMTcwNQYDVQQDEy5TZWN0aWdvIFJTQSBEb21haW4g
 | 
			
		||||
VmFsaWRhdGlvbiBTZWN1cmUgU2VydmVyIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
 | 
			
		||||
AQ8AMIIBCgKCAQEA1nMz1tc8INAA0hdFuNY+B6I/x0HuMjDJsGz99J/LEpgPLT+N
 | 
			
		||||
TQEMgg8Xf2Iu6bhIefsWg06t1zIlk7cHv7lQP6lMw0Aq6Tn/2YHKHxYyQdqAJrkj
 | 
			
		||||
eocgHuP/IJo8lURvh3UGkEC0MpMWCRAIIz7S3YcPb11RFGoKacVPAXJpz9OTTG0E
 | 
			
		||||
oKMbgn6xmrntxZ7FN3ifmgg0+1YuWMQJDgZkW7w33PGfKGioVrCSo1yfu4iYCBsk
 | 
			
		||||
Haswha6vsC6eep3BwEIc4gLw6uBK0u+QDrTBQBbwb4VCSmT3pDCg/r8uoydajotY
 | 
			
		||||
uK3DGReEY+1vVv2Dy2A0xHS+5p3b4eTlygxfFQIDAQABo4IBbjCCAWowHwYDVR0j
 | 
			
		||||
BBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFI2MXsRUrYrhd+mb
 | 
			
		||||
+ZsF4bgBjWHhMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0G
 | 
			
		||||
A1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAbBgNVHSAEFDASMAYGBFUdIAAw
 | 
			
		||||
CAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNlcnRydXN0
 | 
			
		||||
LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDB2Bggr
 | 
			
		||||
BgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRydXN0LmNv
 | 
			
		||||
bS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZaHR0cDov
 | 
			
		||||
L29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAMr9hvQ5Iw0/H
 | 
			
		||||
ukdN+Jx4GQHcEx2Ab/zDcLRSmjEzmldS+zGea6TvVKqJjUAXaPgREHzSyrHxVYbH
 | 
			
		||||
7rM2kYb2OVG/Rr8PoLq0935JxCo2F57kaDl6r5ROVm+yezu/Coa9zcV3HAO4OLGi
 | 
			
		||||
H19+24rcRki2aArPsrW04jTkZ6k4Zgle0rj8nSg6F0AnwnJOKf0hPHzPE/uWLMUx
 | 
			
		||||
RP0T7dWbqWlod3zu4f+k+TY4CFM5ooQ0nBnzvg6s1SQ36yOoeNDT5++SR2RiOSLv
 | 
			
		||||
xvcRviKFxmZEJCaOEDKNyJOuB56DPi/Z+fVGjmO+wea03KbNIaiGCpXZLoUmGv38
 | 
			
		||||
sbZXQm2V0TP2ORQGgkE49Y9Y3IBbpNV9lXj9p5v//cWoaasm56ekBYdbqbe4oyAL
 | 
			
		||||
l6lFhd2zi+WJN44pDfwGF/Y4QA5C5BIG+3vzxhFoYt/jmPQT2BVPi7Fp2RBgvGQq
 | 
			
		||||
6jG35LWjOhSbJuMLe/0CjraZwTiXWTb2qHSihrZe68Zk6s+go/lunrotEbaGmAhY
 | 
			
		||||
LcmsJWTyXnW0OMGuf1pGg+pRyrbxmRE1a6Vqe8YAsOf4vmSyrcjC8azjUeqkk+B5
 | 
			
		||||
yOGBQMkKW+ESPMFgKuOXwIlCypTPRpgSabuY0MLTDXJLR27lk8QyKGOHQ+SwMj4K
 | 
			
		||||
00u/I5sUKUErmgQfky3xxzlIPK1aEn8=
 | 
			
		||||
-----END CERTIFICATE-----
 | 
			
		||||
							
								
								
									
										28
									
								
								static/certs/key.pem
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								static/certs/key.pem
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
-----BEGIN PRIVATE KEY-----
 | 
			
		||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDHuNl1ihybke3a
 | 
			
		||||
BU1+/c6xCn05Q9oY0SIn93RyiTUyTb2WudpiKB3pT+0VVLhToae2mQjS4SBMw8mT
 | 
			
		||||
m5fL8Dui9j1bubWwRBKDYsW6X4XPGSAhAblaNlAIZyRXDCbbr7awqvu8nqHMzP5P
 | 
			
		||||
54fBQc9lpy81rOAmbdbGuZJ2VxZgZrByULUCAur8e0M1XChjpQKLCBB0il+JyM+X
 | 
			
		||||
y4Fks6K9Rmsueo99T05Pqkb5T0DNmioi1p5S30RA+Mxyai3veIrbvJZT12+oPnC8
 | 
			
		||||
cUSlKTKrRv9eXA3QWcL+NA42u25jX3tbuMNkAlS47x1WlaTV23EWsvzIZzNcOS8w
 | 
			
		||||
2ZlefG9fAgMBAAECggEAT1Tti/LASks8300b60WFxG0WMJjzGMh5eMaiSpyVtNWM
 | 
			
		||||
aUKJrFOjDfnhgoeUcCPWKoG/L4Sc/+EFQMydDzTte120IasysEFZ2TZytAUdcZXZ
 | 
			
		||||
XUMCDQNl5vCRTsJU7Q5u0t4YAGRCgMcsfTDKi8lISGiQKBHzN1CJ74Xm13rgOInd
 | 
			
		||||
lAc0wd5S89sL6RYmRTj1LvuZ95EHXHqQGdv0fIFEyP3pF1iPwcoTuIVEeICqnEvW
 | 
			
		||||
vd8CVO68eH3HFIwioqjp4qW3pxPZMhVq4161805uAMkoQlE+7MtEVenmP++1u1gM
 | 
			
		||||
FjvAs3j9CZqOHZKcLlOtcGSwDlD++fCMMT4slLgLgQKBgQDy58E5nuYXdxlFQQk4
 | 
			
		||||
QccUKpyJ2aVXyp9xvTFBot/5Pik1SkuDzv2XU1OTxdxf3EongLy91nMJ2/6/39Je
 | 
			
		||||
lf0/2MjzCtJ/lSzZ/zpJAu86UkBkWBAA5loGIof6OKedbEIgqpJqtK59S+j3ExO9
 | 
			
		||||
eqa+uFrtt1UfaJG4A7TT+dIvIwKBgQDSfSOdSM5Dh3KsQHVnIWcIkzwTtlJlO+rG
 | 
			
		||||
6rDEADxw6Kp8VIL/dq4Foe8yW4VqLVrWUuZsU6jzC9GdnyYi6VaqZ/iSUtGkBMOT
 | 
			
		||||
WTTYhqXlURaQ13jhqdwCZJRbVI72JbXn2OGEv8DgXnk//QKED/8VdKqAzCSr1t1f
 | 
			
		||||
3yfwei0AlQKBgD19KU66yKg7/+umEP1quUiDmOjUbaSRqFcUe3mQD356m9ffnMob
 | 
			
		||||
BdrevxNzTNv/Wc4yKpUryic+x3gu4oQLF/annAbaQHsHejkdANYmpgRvedls6XAw
 | 
			
		||||
360Z5K4U1WlmVD8Mrs/QOTOCmdChxad7euZgqLPwat3ujKS2W3oljW1dAoGBAM4/
 | 
			
		||||
AB6lsDZLCfnuTxt2h1bHrh5CkAnR5AJ1BC+Ja6/WyvZ4eMOIroumWJKnStr3BgLr
 | 
			
		||||
yAxtDSbZddNUljGvIdRnfBEkRXbJlDlVN4rSpMtF4S6bcz7rCUDu/M9g05Qs70j2
 | 
			
		||||
IkPJAFzZNUWVzFlKs096uXbqkSQvrUq7ho8DqAThAoGBAL7Nrbr5LWcBgvwEhEla
 | 
			
		||||
VRfYb0FUrDwLIrVWntJjW566/pVQQ4BmatsblLjlQYWk9MCIYXWZbnB+2fRx9yjQ
 | 
			
		||||
Adggez7Dws/Mrh/wVudKgayHCy5Lgd8rYjNgC+VZf8XGrWX3QXMJ6UWAyQLTeoO7
 | 
			
		||||
hToW9o9CQMIhaR43G8di1kjF
 | 
			
		||||
-----END PRIVATE KEY-----
 | 
			
		||||
@ -1,160 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "/Lotus/Types/Keys/VorsPrize/MissionOne": [
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/SocialMenuFeatureItem"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponMeleeDamageMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponDamageAmountMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponDamageAmountMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_CREDITS",
 | 
			
		||||
      "amount": 2500
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "/Lotus/Types/Keys/VorsPrize/MissionTwo": [
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/ModsFeatureItem"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarHealthMaxMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarShieldMaxMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarAbilityRangeMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarAbilityStrengthMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarAbilityDurationMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_CREDITS",
 | 
			
		||||
      "amount": 2500
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "/Lotus/Types/Keys/VorsPrize/MissionThree": [
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/FoundryFeatureItem"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/WeaponFireRateMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponFactionDamageCorpus"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/WeaponFactionDamageGrineer"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponFireDamageMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/WeaponElectricityDamageMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_CREDITS",
 | 
			
		||||
      "amount": 2500
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "/Lotus/Types/Keys/VorsPrize/MissionFour": [
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarPickupBonusMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarPowerMaxMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/Warframe/AvatarEnemyRadarMod"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_CREDITS",
 | 
			
		||||
      "amount": 2500
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "/Lotus/Types/Keys/VorsPrize/MissionFive": [
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_STORE_ITEM",
 | 
			
		||||
      "itemType": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/CommonFusionBundle"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "rewardType": "RT_CREDITS",
 | 
			
		||||
      "amount": 5000
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -1152,7 +1152,7 @@
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="doHelminthUnlockAll();" data-loc="cheats_helminthUnlockAll"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(addMissingHelminthRecipes);" data-loc="cheats_addMissingSubsumedAbilities"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="doIntrinsicsUnlockAll();" data-loc="cheats_intrinsicsUnlockAll"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(doMaxPlexus);" data-loc="cheats_maxPlexus"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(doMaxPlexus);" data-loc="inventory_maxPlexus"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(unlockAllProfitTakerStages);" data-loc="cheats_unlockAllProfitTakerStages"></button>
 | 
			
		||||
                                    <button class="btn btn-primary" onclick="debounce(unlockAllSimarisResearchEntries);" data-loc="cheats_unlockAllSimarisResearchEntries"></button>
 | 
			
		||||
                                </div>
 | 
			
		||||
@ -1474,7 +1474,6 @@
 | 
			
		||||
                                    <label class="form-label" for="worldState.nightwaveOverride" data-loc="worldState_nightwaveOverride"></label>
 | 
			
		||||
                                    <select class="form-control" id="worldState.nightwaveOverride" data-default="">
 | 
			
		||||
                                        <option value="" data-loc="disabled"></option>
 | 
			
		||||
                                        <option value="RadioLegionIntermission14Syndicate" data-loc="worldState_RadioLegionIntermission14Syndicate"></option>
 | 
			
		||||
                                        <option value="RadioLegionIntermission13Syndicate" data-loc="worldState_RadioLegionIntermission13Syndicate"></option>
 | 
			
		||||
                                        <option value="RadioLegionIntermission12Syndicate" data-loc="worldState_RadioLegionIntermission12Syndicate"></option>
 | 
			
		||||
                                        <option value="RadioLegionIntermission11Syndicate" data-loc="worldState_RadioLegionIntermission11Syndicate"></option>
 | 
			
		||||
 | 
			
		||||
@ -169,7 +169,6 @@ function renameAccount(taken_name) {
 | 
			
		||||
                } else {
 | 
			
		||||
                    $(".displayname").text(newname);
 | 
			
		||||
                    updateLocElements();
 | 
			
		||||
                    toast(loc("code_succRelog"));
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
@ -560,7 +559,7 @@ function fetchItemList() {
 | 
			
		||||
                    });
 | 
			
		||||
                } else if (type == "Syndicates") {
 | 
			
		||||
                    items.forEach(item => {
 | 
			
		||||
                        if (["ConclaveSyndicate", "NightcapJournalSyndicate"].includes(item.uniqueName)) {
 | 
			
		||||
                        if (item.uniqueName === "ConclaveSyndicate") {
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                        if (item.uniqueName.startsWith("RadioLegion")) {
 | 
			
		||||
@ -647,7 +646,7 @@ function fetchItemList() {
 | 
			
		||||
                        if ("badReason" in item) {
 | 
			
		||||
                            if (item.badReason == "starter") {
 | 
			
		||||
                                item.name = loc("code_starter").split("|MOD|").join(item.name);
 | 
			
		||||
                            } else if (item.badReason != "notraw") {
 | 
			
		||||
                            } else {
 | 
			
		||||
                                item.name += " " + loc("code_badItem");
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
@ -1455,11 +1454,7 @@ function updateInventory() {
 | 
			
		||||
                {
 | 
			
		||||
                    const td = document.createElement("td");
 | 
			
		||||
                    td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
 | 
			
		||||
                    if (itemMap[item.ItemType]?.badReason == "notraw") {
 | 
			
		||||
                        // Assuming this is a riven with a pending challenge, so rank would be N/A, but otherwise it's fine.
 | 
			
		||||
                    } else {
 | 
			
		||||
                        td.innerHTML += " <span title='" + loc("code_rank") + "'>★ " + rank + "/" + maxRank + "</span>";
 | 
			
		||||
                    }
 | 
			
		||||
                    td.innerHTML += " <span title='" + loc("code_rank") + "'>★ " + rank + "/" + maxRank + "</span>";
 | 
			
		||||
                    tr.appendChild(td);
 | 
			
		||||
                }
 | 
			
		||||
                {
 | 
			
		||||
@ -1547,17 +1542,14 @@ function updateInventory() {
 | 
			
		||||
 | 
			
		||||
                if (item) {
 | 
			
		||||
                    document.getElementById("detailedView-loading").classList.add("d-none");
 | 
			
		||||
                    const itemName = itemMap[item.ItemType]?.name ?? item.ItemType;
 | 
			
		||||
 | 
			
		||||
                    if (item.ItemName) {
 | 
			
		||||
                        const pipeIndex = item.ItemName.indexOf("|");
 | 
			
		||||
                        if (pipeIndex != -1) {
 | 
			
		||||
                            $("#detailedView-title").text(item.ItemName.substr(1 + pipeIndex) + " " + itemName);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            $("#detailedView-title").text(item.ItemName);
 | 
			
		||||
                            $("#detailedView-route .text-body-secondary").text(itemName);
 | 
			
		||||
                        }
 | 
			
		||||
                        $("#detailedView-title").text(item.ItemName);
 | 
			
		||||
                        $("#detailedView-route .text-body-secondary").text(
 | 
			
		||||
                            itemMap[item.ItemType]?.name ?? item.ItemType
 | 
			
		||||
                        );
 | 
			
		||||
                    } else {
 | 
			
		||||
                        $("#detailedView-title").text(itemName);
 | 
			
		||||
                        $("#detailedView-title").text(itemMap[item.ItemType]?.name ?? item.ItemType);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (category == "Suits") {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// German translation by Animan8000
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Hinweis: Um Änderungen im Spiel zu sehen, musst du dein Inventar neu synchronisieren, z. B. durch Besuch eines Dojo/Relais oder durch erneutes Anmelden.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Hinweis: Um Änderungen im Spiel zu sehen, musst du dein Inventar neu synchronisieren, z. B. mit dem /sync Befehl des Bootstrappers im Spielchat, durch Besuch eines Dojo/Relais oder durch erneutes Einloggen.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Hinweis: Möglicherweise musst du ein Menü neu öffnen, damit die Änderungen sichtbar werden.`,
 | 
			
		||||
    general_addButton: `Hinzufügen`,
 | 
			
		||||
    general_setButton: `Festlegen`,
 | 
			
		||||
@ -51,7 +51,7 @@ dict = {
 | 
			
		||||
    code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden.`,
 | 
			
		||||
    code_addModsConfirm: `Bist du sicher, dass du |COUNT| Mods zu deinem Account hinzufügen möchtest?`,
 | 
			
		||||
    code_succImport: `Erfolgreich importiert.`,
 | 
			
		||||
    code_succRelog: `Fertig. Bitte beachte, dass du dich neu anmelden musst, damit die Änderung im Spiel sichtbar wird.`,
 | 
			
		||||
    code_succRelog: `Fertig. Bitte beachte, dass du dich neu einloggen musst, um Änderungen im Spiel zu sehen.`,
 | 
			
		||||
    code_nothingToDo: `Fertig. Es gab nichts zu tun.`,
 | 
			
		||||
    code_gild: `Veredeln`,
 | 
			
		||||
    code_moa: `Moa`,
 | 
			
		||||
@ -134,7 +134,8 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`,
 | 
			
		||||
    inventory_removeIsNew: `Entferne Ausrufezeichen bei neuem Equipment`,
 | 
			
		||||
    inventory_maxPlexus: `Plexus auf Max. Rang`,
 | 
			
		||||
    inventory_removeIsNew: `[UNTRANSLATED] Remove New Equipment Exclamation Icon`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `Quests`,
 | 
			
		||||
    quests_completeAll: `Alle Quests abschließen`,
 | 
			
		||||
@ -199,9 +200,9 @@ dict = {
 | 
			
		||||
    cheats_skipTutorial: `Tutorial überspringen`,
 | 
			
		||||
    cheats_skipAllDialogue: `Alle Dialoge überspringen`,
 | 
			
		||||
    cheats_unlockAllScans: `Alle Scans freischalten`,
 | 
			
		||||
    cheats_unlockSuccRelog: `Erfolgreich. Bitte beachte, dass du dich neu anmelden musst, damit der Client dies aktualisiert.`,
 | 
			
		||||
    cheats_unlockSuccRelog: `Erfolgreich. Bitte beachte, dass du dich neu einloggen musst, damit der Client dies aktualisiert.`,
 | 
			
		||||
    cheats_unlockAllMissions: `Alle Missionen freischalten`,
 | 
			
		||||
    cheats_unlockAllMissions_ok: `Erfolgreich. Bitte beachte, dass du ein Dojo/Relais besuchen oder dich neu anmelden musst, damit die Sternenkarte aktualisiert wird.`,
 | 
			
		||||
    cheats_unlockAllMissions_ok: `Erfolgreich. Bitte beachte, dass du ein Dojo/Relais besuchen oder dich neu einloggen musst, damit die Sternenkarte aktualisiert wird.`,
 | 
			
		||||
    cheats_infiniteCredits: `Unendlich Credits`,
 | 
			
		||||
    cheats_infinitePlatinum: `Unendlich Platinum`,
 | 
			
		||||
    cheats_infiniteEndo: `Unendlich Endo`,
 | 
			
		||||
@ -213,7 +214,7 @@ dict = {
 | 
			
		||||
    cheats_dontSubtractPurchaseItemCost: `Gegenstände beim Kauf nicht verbrauchen`,
 | 
			
		||||
    cheats_dontSubtractPurchaseStandingCost: `Ansehen beim Kauf nicht verbrauchen`,
 | 
			
		||||
    cheats_dontSubtractVoidTraces: `Void-Spuren nicht verbrauchen`,
 | 
			
		||||
    cheats_dontSubtractConsumables: `Verbrauchsgüter (Ausrüstung) nicht verbrauchen`,
 | 
			
		||||
    cheats_dontSubtractConsumables: `Verbrauchsgegenstände (Ausrüstung) nicht verbrauchen`,
 | 
			
		||||
    cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`,
 | 
			
		||||
    cheats_unlockAllSkins: `Alle Skins freischalten`,
 | 
			
		||||
    cheats_unlockAllCapturaScenes: `Alle Photora-Szenen freischalten`,
 | 
			
		||||
@ -233,7 +234,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Baro hat volles Inventar`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Alle Profiteintreiber-Phasen freischalten`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Erfolgreich. Bitte beachte, dass du dein Inventar neu synchronisieren musst, z. B. durch Besuch eines Dojo/Relais oder durch erneutes Anmelden.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Erfolgreich. Bitte beachte, dass du dein Inventar neu synchronisieren musst, z. B. mit dem /sync Befehl des Bootstrappers im Spielchat, durch Besuch eines Dojo/Relais oder durch erneutes Einloggen.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`,
 | 
			
		||||
@ -259,7 +260,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `Helminth vollständig aufleveln`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `Fehlende konsumierte Fähigkeiten hinzufügen`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `Alle Inhärenzen auf Max. Rang`,
 | 
			
		||||
    cheats_maxPlexus: `Plexus auf Max. Rang`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `Unterstütztes Syndikat`,
 | 
			
		||||
    cheats_changeButton: `Ändern`,
 | 
			
		||||
    cheats_markAllAsRead: `Posteingang als gelesen markieren`,
 | 
			
		||||
@ -324,7 +324,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `Trauer`,
 | 
			
		||||
    worldState_fear: `Angst`,
 | 
			
		||||
    worldState_nightwaveOverride: `Nightwave-Überschreibung`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `Noras Mix: Träume der Toten`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `Noras Mix - Vol. 9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `Noras Mix - Vol. 8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `Noras Mix - Vol. 7`,
 | 
			
		||||
@ -386,10 +385,10 @@ dict = {
 | 
			
		||||
    upgrade_AvatarAbilityRange: `+7.5% Fähigkeitsreichweite`,
 | 
			
		||||
    upgrade_AvatarAbilityEfficiency: `+5% Fähigkeitseffizienz`,
 | 
			
		||||
    upgrade_AvatarEnergyRegen: `+0.5 Energieregeneration pro Sekunde`,
 | 
			
		||||
    upgrade_AvatarEnemyRadar: `+5m Gegnerradar`,
 | 
			
		||||
    upgrade_AvatarEnemyRadar: `+5m Feindradar`,
 | 
			
		||||
    upgrade_AvatarLootRadar: `+7m Beuteradar`,
 | 
			
		||||
    upgrade_WeaponAmmoMax: `+15% Max. Munition`,
 | 
			
		||||
    upgrade_EnemyArmorReductionAura: `-3% Rüstung für Gegner`,
 | 
			
		||||
    upgrade_EnemyArmorReductionAura: `-3% Rüstung bei Feinden`,
 | 
			
		||||
    upgrade_OnExecutionAmmo: `+100% Magazinfüllung für Primär- und Sekundärwaffen bei Gnadenstoß`,
 | 
			
		||||
    upgrade_OnExecutionHealthDrop: `+100% Gesundheitskugel Chance bei Gnadenstoß`,
 | 
			
		||||
    upgrade_OnExecutionEnergyDrop: `+50% Energiekugel Chance bei Gnadenstoß`,
 | 
			
		||||
@ -399,7 +398,7 @@ dict = {
 | 
			
		||||
    upgrade_OnExecutionParkourSpeed: `+60% Parkourgeschwindigkeit für 15s nach Gnadenstoß`,
 | 
			
		||||
    upgrade_AvatarTimeLimitIncrease: `+8s extra Zeit beim Hacken`,
 | 
			
		||||
    upgrade_ElectrifyOnHack: `Setze beim Hacken Gegner innerhalb von 20m unter Strom`,
 | 
			
		||||
    upgrade_OnExecutionTerrify: `+50% Chance bei Gnadenstoß, dass Gegner innerhalb von 15m vor Furcht für 8s kauern`,
 | 
			
		||||
    upgrade_OnExecutionTerrify: `+50% Chance bei Gnadenstoß, dass Feinde innerhalb von 15m vor Furcht für 8s kauern`,
 | 
			
		||||
    upgrade_OnHackLockers: `Schließe nach dem Hacken 5 Spinde innerhalb von 20m auf`,
 | 
			
		||||
    upgrade_OnExecutionBlind: `Blende bei einem Gnadenstoß Gegner innerhalb von 18m`,
 | 
			
		||||
    upgrade_OnExecutionDrainPower: `Nächste Fähigkeit erhält +50% Fähigkeitsstärke nach Gnadenstoß`,
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `Add`,
 | 
			
		||||
    general_setButton: `Set`,
 | 
			
		||||
@ -133,6 +133,7 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `Max Rank All Sentinels`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`,
 | 
			
		||||
    inventory_maxPlexus: `Max Rank Plexus`,
 | 
			
		||||
    inventory_removeIsNew: `Remove New Equipment Exclamation Icon`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `Quests`,
 | 
			
		||||
@ -232,7 +233,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Baro Fully Stocked`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`,
 | 
			
		||||
@ -258,7 +259,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `Fully Level Up Helminth`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `Add Missing Subsumed Abilities`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `Max Rank All Intrinsics`,
 | 
			
		||||
    cheats_maxPlexus: `Max Rank Plexus`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `Supported syndicate`,
 | 
			
		||||
    cheats_changeButton: `Change`,
 | 
			
		||||
    cheats_markAllAsRead: `Mark Inbox As Read`,
 | 
			
		||||
@ -323,7 +323,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `Sorrow`,
 | 
			
		||||
    worldState_fear: `Fear`,
 | 
			
		||||
    worldState_nightwaveOverride: `Nightwave Override`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `Nora's Mix: Dreams of the Dead`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `Nora's Mix Vol. 9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `Nora's Mix Vol. 8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `Nora's Mix Vol. 7`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Spanish translation by hxedcl, Slayer55555
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Para ver los cambios en el juego, necesitas volver a sincronizar tu inventario, por ejemplo, usando el comando /sync del bootstrapper, visitando un dojo o repetidor, o volviendo a iniciar sesión.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Nota: Puede que necesites reabrir cualquier menú en el que te encuentres para que los cambios se reflejen.`,
 | 
			
		||||
    general_addButton: `Agregar`,
 | 
			
		||||
    general_setButton: `Establecer`,
 | 
			
		||||
@ -134,6 +134,7 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`,
 | 
			
		||||
    inventory_maxPlexus: `Rango máximo de Plexus`,
 | 
			
		||||
    inventory_removeIsNew: `[UNTRANSLATED] Remove New Equipment Exclamation Icon`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `Misiones`,
 | 
			
		||||
@ -233,7 +234,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Baro con stock completo`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Misiones de sindicato rejugables`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Desbloquea todas las etapas del Roba-ganancias`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Éxito. Ten en cuenta que deberás volver a sincronizar tu inventario. Para hacerlo, puedes usar el comando /sync en el Bootstrapper, visitar un dojo o repetidor, o volver a iniciar sesión.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Terminar desafío de agrietado inmediatamente`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`,
 | 
			
		||||
@ -259,7 +260,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `Subir al máximo el Helminto`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `Agregar habilidades subsumidas faltantes`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `Maximizar todos los intrínsecos`,
 | 
			
		||||
    cheats_maxPlexus: `Rango máximo de Plexus`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `Sindicatos disponibles`,
 | 
			
		||||
    cheats_changeButton: `Cambiar`,
 | 
			
		||||
    cheats_markAllAsRead: `Marcar bandeja de entrada como leída`,
 | 
			
		||||
@ -324,7 +324,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `Tristeza`,
 | 
			
		||||
    worldState_fear: `Miedo`,
 | 
			
		||||
    worldState_nightwaveOverride: `Volúmen de Onda Nocturna`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `[UNTRANSLATED] Nora's Mix: Dreams of the Dead`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `Mix de Nora Vol. 9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `Mix de Nora Vol. 8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `Mix de Nora Vol. 7`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// French translation by Vitruvio
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Note : Pour voir les changements en jeu, l'inventaire doit être actualisé. Cela se fait en tapant /sync dans le tchat, en visitant un dojo/relais ou en se reconnectant.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Note : Rouvrir un menu est nécessaire pour voir les changements.`,
 | 
			
		||||
    general_addButton: `Ajouter`,
 | 
			
		||||
    general_setButton: `Définir`,
 | 
			
		||||
@ -134,6 +134,7 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `Toutes les Sentinelles au rang max`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles au rang max`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `Toutes les évolutions Incarnon au rang max`,
 | 
			
		||||
    inventory_maxPlexus: `Plexus au rang max`,
 | 
			
		||||
    inventory_removeIsNew: `[UNTRANSLATED] Remove New Equipment Exclamation Icon`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `Quêtes`,
 | 
			
		||||
@ -233,7 +234,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Stock de Baro au max`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Débloquer toutes les étapes du Preneur de Profit`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Succès. Une resynchronisation est nécessaire en tapant "/sync" dans le tchat, en visitant un relais ou en se reconnectant.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`,
 | 
			
		||||
@ -259,7 +260,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `Helminth niveau max`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `Ajouter les capacités subsumées manquantes`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `Inhérences niveau max`,
 | 
			
		||||
    cheats_maxPlexus: `Plexus au rang max`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `Allégeance`,
 | 
			
		||||
    cheats_changeButton: `Changer`,
 | 
			
		||||
    cheats_markAllAsRead: `Marquer la boîte de réception comme lue`,
 | 
			
		||||
@ -324,7 +324,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `hagrin`,
 | 
			
		||||
    worldState_fear: `Peur`,
 | 
			
		||||
    worldState_nightwaveOverride: `Saison d'Ondes Nocturnes`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `[UNTRANSLATED] Nora's Mix: Dreams of the Dead`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `Mix de Nora Vol. 9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `Mix de Nora Vol. 8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `Mix de Nora Vol. 7`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Russian translation by AMelonInsideLemon, LoseFace
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Примечание: Чтобы увидеть изменения в игре, вам нужно повторно синхронизировать свой инвентарь, например, используя команду загрузчика /sync в чате игры, посетив Додзё/Реле или перезагрузив игру.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Примечание: для того, чтобы изменения вступили в силу, может потребоваться повторно открыть меню, в котором вы находитесь.`,
 | 
			
		||||
    general_addButton: `Добавить`,
 | 
			
		||||
    general_setButton: `Установить`,
 | 
			
		||||
@ -134,6 +134,7 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `Макс. ранг всех Стражей`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `Макс. ранг всего оружия Стражей`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `Макс. ранг всех эволюций Инкарнонов`,
 | 
			
		||||
    inventory_maxPlexus: `Макс. ранг Плексуса`,
 | 
			
		||||
    inventory_removeIsNew: `Удалить значок восклицательного знака нового снаряжения`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `Квесты`,
 | 
			
		||||
@ -233,7 +234,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Баро полностью укомплектован`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Повторять миссии синдиката`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Разблокировать все этапы Сферы извлечения прибыли`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Успех. Обратите внимание, что вам необходимо будет повторно синхронизировать свой инвентарь, например, с помощью команды загрузчика /sync в чате игры, посетив Додзё/Реле или повторно войдя в игру.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Мгновенное завершение испытания мода Разлома`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Мгновенно добывающие Дроны-сборщики`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Без урона по Дронам-сборщикам`,
 | 
			
		||||
@ -259,7 +260,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `Полностью улучшить Гельминта`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `Добавить отсутствующие поглощённые способности`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `Полностью улучшить Модуляры`,
 | 
			
		||||
    cheats_maxPlexus: `Макс. ранг Плексуса`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `Поддерживаемый синдикат`,
 | 
			
		||||
    cheats_changeButton: `Изменить`,
 | 
			
		||||
    cheats_markAllAsRead: `Пометить все входящие как прочитанные`,
 | 
			
		||||
@ -324,7 +324,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `Печаль`,
 | 
			
		||||
    worldState_fear: `Страх`,
 | 
			
		||||
    worldState_nightwaveOverride: `Сезон Ночной волны`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `[UNTRANSLATED] Nora's Mix: Dreams of the Dead`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `Микс Норы, Диск 9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `Микс Норы, Диск 8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `Микс Норы, Диск 7`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Ukrainian translation by LoseFace
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `Пам'ятка: Щоб побачити зміни в грі, вам потрібно повторно синхронізувати своє спорядження, наприклад, використовуючи команду завантажувача /sync у чаті гри, відвідавши Доджьо/Реле або перезавантаживши гру.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `Примітка: для відображення змін може знадобитися повторно відкрити меню, в якому ви перебуваєте.`,
 | 
			
		||||
    general_addButton: `Добавити`,
 | 
			
		||||
    general_setButton: `Встановити`,
 | 
			
		||||
@ -134,6 +134,7 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `Макс. рівень всіх Вартових`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `Макс. рівень всієї зброї Вартових`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `Макс. рівень всіх еволюцій Інкарнонів`,
 | 
			
		||||
    inventory_maxPlexus: `Макс. рівень Плексу`,
 | 
			
		||||
    inventory_removeIsNew: `[UNTRANSLATED] Remove New Equipment Exclamation Icon`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `Пригоди`,
 | 
			
		||||
@ -233,7 +234,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `Баро повністю укомплектований`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `Повторювати місії синдиката`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `Розблокувати всі етапи Привласнювачки`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `Успішно. Зверніть увагу, що вам потрібно буде повторно синхронізувати своє спорядження, наприклад, за допомогою команди завантажувача /sync у чаті гри, відвідавши Доджьо/Реле або повторно увійшовши в гру.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `Миттєве завершення випробування модифікатора Розколу`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `Миттєво добуваючі Дрони-видобувачі`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `Без шкоди по Дронам-видобувачам`,
 | 
			
		||||
@ -259,7 +260,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `Повністю покращити Гельмінта`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `Додати відсутні поглинуті здібності`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `Повністю покращити Кваліфікації`,
 | 
			
		||||
    cheats_maxPlexus: `Макс. рівень Плексу`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `Підтримуваний синдикат`,
 | 
			
		||||
    cheats_changeButton: `Змінити`,
 | 
			
		||||
    cheats_markAllAsRead: `Помітити всі вхідні як прочитані`,
 | 
			
		||||
@ -324,7 +324,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `Журба`,
 | 
			
		||||
    worldState_fear: `Страх`,
 | 
			
		||||
    worldState_nightwaveOverride: `Сезон Нічної хвилі`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `[UNTRANSLATED] Nora's Mix: Dreams of the Dead`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `Вибірка Нори 9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `Вибірка Нори 8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `Вибірка Нори 7`,
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
// Chinese translation by meb154, bishan178, nyaoouo, qianlishun, CrazyZhang, Corvus, qingchun
 | 
			
		||||
dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    general_inventoryUpdateNote: `注意: 要在游戏中查看更改,您需要重新同步库存,例如使用客户端的 /sync 命令,访问道场/中继站或重新登录.`,
 | 
			
		||||
    general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`,
 | 
			
		||||
    general_addButton: `添加`,
 | 
			
		||||
    general_setButton: `设置`,
 | 
			
		||||
@ -134,6 +134,7 @@ dict = {
 | 
			
		||||
    inventory_bulkRankUpSentinels: `所有守护升满级`,
 | 
			
		||||
    inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`,
 | 
			
		||||
    inventory_bulkRankUpEvolutionProgress: `所有灵化之源进度最大等级`,
 | 
			
		||||
    inventory_maxPlexus: `最大深控等级`,
 | 
			
		||||
    inventory_removeIsNew: `[UNTRANSLATED] Remove New Equipment Exclamation Icon`,
 | 
			
		||||
 | 
			
		||||
    quests_list: `系列任务`,
 | 
			
		||||
@ -233,7 +234,7 @@ dict = {
 | 
			
		||||
    cheats_baroFullyStocked: `虚空商人贩卖所有商品`,
 | 
			
		||||
    cheats_syndicateMissionsRepeatable: `集团任务可重复完成`,
 | 
			
		||||
    cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. by visiting a dojo/relay or relogging.`,
 | 
			
		||||
    cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`,
 | 
			
		||||
    cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`,
 | 
			
		||||
    cheats_instantResourceExtractorDrones: `资源无人机即时完成`,
 | 
			
		||||
    cheats_noResourceExtractorDronesDamage: `资源无人机不会损毁`,
 | 
			
		||||
@ -259,7 +260,6 @@ dict = {
 | 
			
		||||
    cheats_helminthUnlockAll: `完全升级Helminth`,
 | 
			
		||||
    cheats_addMissingSubsumedAbilities: `添加Helminth未汲取的战甲技能`,
 | 
			
		||||
    cheats_intrinsicsUnlockAll: `所有内源之力最大等级`,
 | 
			
		||||
    cheats_maxPlexus: `最大深控等级`,
 | 
			
		||||
    cheats_changeSupportedSyndicate: `支持的集团`,
 | 
			
		||||
    cheats_changeButton: `更改`,
 | 
			
		||||
    cheats_markAllAsRead: `收件箱全部标记为已读`,
 | 
			
		||||
@ -324,7 +324,6 @@ dict = {
 | 
			
		||||
    worldState_sorrow: `悲伤`,
 | 
			
		||||
    worldState_fear: `恐惧`,
 | 
			
		||||
    worldState_nightwaveOverride: `午夜电波系列`,
 | 
			
		||||
    worldState_RadioLegionIntermission14Syndicate: `[UNTRANSLATED] Nora's Mix: Dreams of the Dead`,
 | 
			
		||||
    worldState_RadioLegionIntermission13Syndicate: `诺拉的混选VOL.9`,
 | 
			
		||||
    worldState_RadioLegionIntermission12Syndicate: `诺拉的混选VOL.8`,
 | 
			
		||||
    worldState_RadioLegionIntermission11Syndicate: `诺拉的混选VOL.7`,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user