Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

151 changed files with 2525 additions and 7153 deletions

View File

@ -31,8 +31,7 @@
"no-mixed-spaces-and-tabs": "error",
"@typescript-eslint/require-await": "error",
"import/no-named-as-default-member": "off",
"import/no-cycle": "warn",
"@typescript-eslint/no-deprecated": "warn"
"import/no-cycle": "warn"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {

View File

@ -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

17
AGENTS.md Normal file
View File

@ -0,0 +1,17 @@
## In General
### Prerequisites
Use `npm i` or `npm ci` to install all dependencies.
### Testing
Use `npm run verify` to verify that your changes pass TypeScript's checks.
### Formatting
Use `npm run prettier` to ensure your formatting matches the expected format. Failing to do so will cause CI failure.
## WebUI Specific
The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT produce non-English strings; we want them to be translated by humans who can understand the full context.

View File

@ -1,19 +0,0 @@
## In General
### Prerequisites
Use `npm i` or `npm ci` to install all dependencies, including dev dependencies.
## Development Process
Auto reloading is supported for server and WebUI development. Simply use `npm run dev` or `npm run dev:bun` to start the server and edit away.
### Testing
Before submitting a PR:
- Use `npm run verify` to verify that the code is type-safe.
- Use `npm run fix` to fix formatting issues as well as be informed of any unfixable issues. Avoid introducing new warnings.
## WebUI Specific
The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT provide translations generated by AI or other automated tools.

View File

@ -6,21 +6,18 @@ More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://di
This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled.
- `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.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `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
@ -37,5 +34,5 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six
- `worldState.allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.

View File

@ -14,18 +14,13 @@ if %errorlevel% == 0 (
)
echo Updating dependencies...
node scripts/raw-precheck.js > NUL
call npm i --omit=dev
call npm run build
if %errorlevel% == 0 (
call npm i --omit=dev --omit=optional
call npm run raw
) else (
call npm i --omit=dev
call npm run build
if %errorlevel% == 0 (
call npm run start
)
call npm run start
echo SpaceNinjaServer seems to have crashed.
)
echo SpaceNinjaServer seems to have crashed.
)
:a

View File

@ -14,16 +14,11 @@ if [ $? -eq 0 ]; then
fi
echo "Updating dependencies..."
node scripts/raw-precheck.js > /dev/null
npm i --omit=dev
npm run build
if [ $? -eq 0 ]; then
npm i --omit=dev --omit=optional
npm run raw
else
npm i --omit=dev
npm run build
if [ $? -eq 0 ]; then
npm run start
fi
npm run start
echo "SpaceNinjaServer seems to have crashed."
fi
echo "SpaceNinjaServer seems to have crashed."
fi

View File

@ -5,19 +5,28 @@
"level": "trace"
},
"myAddress": "localhost",
"bindAddress": "0.0.0.0",
"httpPort": 80,
"httpsPort": 443,
"ircExecutable": null,
"ircAddress": null,
"hubAddress": null,
"nrsAddress": null,
"administratorNames": [],
"autoCreateAccount": true,
"skipTutorial": false,
"unlockAllScans": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
"unlockAllSkins": false,
"unlockAllCapturaScenes": false,
"fullyStockedVendors": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDojoDecoBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"relicRewardItemCountMultiplier": 1,
"nightwaveStandingMultiplier": 1,
"unfaithfulBugFixes": {
"ignore1999LastRegionPlayed": false,
"fixXtraCheeseTimer": false,
@ -32,21 +41,15 @@
"baroAlwaysAvailable": false,
"baroFullyStocked": false,
"varziaFullyStocked": false,
"wolfHunt": null,
"wolfHunt": false,
"orphixVenom": false,
"longShadow": false,
"hallowedFlame": false,
"anniversary": null,
"hallowedNightmares": false,
"hallowedNightmaresRewardsOverride": 0,
"naberusNightsOverride": null,
"proxyRebellion": false,
"proxyRebellionRewardsOverride": 0,
"voidCorruption2025Week1": false,
"voidCorruption2025Week2": false,
"voidCorruption2025Week3": false,
"voidCorruption2025Week4": false,
"qtccAlerts": false,
"galleonOfGhouls": 0,
"ghoulEmergenceOverride": null,
"plagueStarOverride": null,

View File

@ -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

View File

@ -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

13
package-lock.json generated
View File

@ -9,7 +9,6 @@
"version": "0.1.0",
"license": "GNU",
"dependencies": {
"body-parser": "^2.2.0",
"chokidar": "^4.0.3",
"crc-32": "^1.2.2",
"express": "^5",
@ -18,7 +17,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.83",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
@ -34,11 +33,7 @@
"prettier": "^3.5.3",
"tree-kill": "^1.2.2"
},
"engines": {
"node": ">=20.18.1"
},
"optionalDependencies": {
"@types/body-parser": "^1.19.6",
"@types/express": "^5",
"@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10",
@ -5534,9 +5529,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.84",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.84.tgz",
"integrity": "sha512-ZpI1Y5CgWDmCwM4/oQpv9u0GD6KFvsJ9f1vJVXYhm5VD9DdOJcFzXgXgg98HXJ5JHbO16ZGIj83117qdpd0RQA=="
},
"node_modules/warframe-riven-info": {
"version": "0.1.2",

View File

@ -5,28 +5,28 @@
"main": "index.ts",
"scripts": {
"start": "node --enable-source-maps build/src/index.js",
"build": "tsgo --inlineSourceMap && ncp static/webui build/static/webui",
"build:tsc": "tsc --incremental --inlineSourceMap && ncp static/webui build/static/webui",
"build:dev": "tsgo --inlineSourceMap",
"build:dev:tsc": "tsc --incremental --inlineSourceMap",
"build": "tsgo --sourceMap && ncp static/webui build/static/webui",
"build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"build:dev": "tsgo --sourceMap",
"build:dev:tsc": "tsc --incremental --sourceMap",
"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",
"verify:tsc": "tsc --noEmit",
"raw": "node scripts/raw-precheck.js && node --experimental-transform-types src/index.ts",
"raw": "node --experimental-transform-types src/index.ts",
"raw:bun": "bun src/index.ts",
"lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.cjs",
"fix": "npm run update-translations && npm run prettier"
"fix": "npm run update-translations && npm run lint:fix"
},
"license": "GNU",
"type": "module",
"dependencies": {
"body-parser": "^2.2.0",
"chokidar": "^4.0.3",
"crc-32": "^1.2.2",
"express": "^5",
@ -35,14 +35,13 @@
"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.83",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"ws": "^8.18.2"
},
"optionalDependencies": {
"@types/body-parser": "^1.19.6",
"@types/express": "^5",
"@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10",

View File

@ -13,17 +13,6 @@ args.push("--dev");
args.push("--secret");
args.push(secret);
const cangoraw = (() => {
if (process.versions.bun) {
return true;
}
const [major, minor] = process.versions.node.split(".").map(x => parseInt(x));
if (major > 22 || (major == 22 && minor >= 7)) {
return true;
}
return false;
})();
let buildproc, runproc;
const spawnopts = { stdio: "inherit", shell: true };
function run(changedFile) {
@ -40,10 +29,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", process.versions.bun ? "verify" : "build:dev"], spawnopts);
const thisbuildstart = Date.now();
buildproc = thisbuildproc;
buildproc.on("exit", code => {
@ -52,17 +38,8 @@ function run(changedFile) {
}
buildproc = undefined;
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(" "),
spawnopts
);
console.log(`${process.versions.bun ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
runproc = spawn("npm", ["run", process.versions.bun ? "raw:bun" : "start", "--", ...args], spawnopts);
runproc.on("exit", () => {
runproc = undefined;
});

View File

@ -1,9 +0,0 @@
const [major, minor] = process.versions.node.split(".").map(x => parseInt(x));
if (major > 22 || (major == 22 && minor >= 7)) {
// ok
} else {
console.log("Sorry, your Node version is a bit too old for this. You have 2 options:");
console.log("- Update Node.js.");
console.log("- Use 'npm run build && npm run start'. Optional libraries must be installed for this.");
process.exit(1);
}

View File

@ -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;
}

View File

@ -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;

View File

@ -31,13 +31,12 @@ export const abortDojoComponentController: RequestHandler = async (req, res) =>
if (request.DecoId) {
removeDojoDeco(guild, request.ComponentId, request.DecoId);
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
} else {
await removeDojoRoom(guild, request.ComponentId);
await guild.save();
res.json(await getDojoClient(guild, 0));
}
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
};
interface IAbortDojoComponentRequest {

View File

@ -3,7 +3,6 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import { addMods, getInventory } from "../../services/inventoryService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -58,7 +57,6 @@ export const artifactsController: RequestHandler = async (req, res) => {
}
res.send(itemId);
broadcastInventoryUpdate(req);
};
interface IArtifactsRequest {

View File

@ -95,7 +95,10 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req,
await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
// Update inventory of new member
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes");
const inventory = await getInventory(
guildMember.accountId.toString(),
"GuildId LevelKeys Recipes skipClanKeyCrafting"
);
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory);
await inventory.save();

View File

@ -5,23 +5,11 @@ import { Guild, GuildMember } from "../../models/guildModel.ts";
import { createUniqueClanName, getGuildClient, giveClanKey } from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
export const createGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId);
if (guild) {
res.json({
...(await getGuildClient(guild, account))
});
return;
}
}
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId: account._id, status: 1 });
@ -39,6 +27,7 @@ export const createGuildController: RequestHandler = async (req, res) => {
rank: 0
});
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes skipClanKeyCrafting");
inventory.GuildId = guild._id;
const inventoryChanges: IInventoryChanges = {};
giveClanKey(inventory, inventoryChanges);
@ -48,7 +37,6 @@ export const createGuildController: RequestHandler = async (req, res) => {
...(await getGuildClient(guild, account)),
InventoryChanges: inventoryChanges
});
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
};
interface ICreateGuildRequest {

View File

@ -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;
}

View File

@ -1,91 +1,23 @@
import type { RequestHandler } from "express";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory, addMiscItems, addEquipment, occupySlot } from "../../services/inventoryService.ts";
import type { IMiscItem, TFocusPolarity, TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { logger } from "../../utils/logger.ts";
import { ExportFocusUpgrades } from "warframe-public-export-plus";
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
import { version_compare } from "../../helpers/inventoryHelpers.ts";
export const focusController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
let op = req.query.op as string;
const focus2 = account.BuildLabel && version_compare(account.BuildLabel, "2022.04.29.12.53") < 0;
if (focus2) {
// Focus 2.0
switch (req.query.op) {
case Focus2Operation.InstallLens:
op = "InstallLens";
break;
case Focus2Operation.UnlockWay:
op = "UnlockWay";
break;
case Focus2Operation.UnlockUpgrade:
op = "UnlockUpgrade";
break;
case Focus2Operation.IncreasePool:
op = "IncreasePool";
break;
case Focus2Operation.LevelUpUpgrade:
op = "LevelUpUpgrade";
break;
case Focus2Operation.ActivateWay:
op = "ActivateWay";
break;
case Focus2Operation.UpdateUpgrade:
op = "UpdateUpgrade";
break;
case Focus2Operation.SentTrainingAmplifier:
op = "SentTrainingAmplifier";
break;
case Focus2Operation.UnbindUpgrade:
op = "UnbindUpgrade";
break;
case Focus2Operation.ConvertShard:
op = "ConvertShard";
break;
}
} else {
// Focus 3.0
switch (req.query.op) {
case Focus3Operation.InstallLens:
op = "InstallLens";
break;
case Focus3Operation.UnlockWay:
op = "UnlockWay";
break;
case Focus3Operation.UnlockUpgrade:
op = "UnlockUpgrade";
break;
case Focus3Operation.LevelUpUpgrade:
op = "LevelUpUpgrade";
break;
case Focus3Operation.ActivateWay:
op = "ActivateWay";
break;
case Focus3Operation.SentTrainingAmplifier:
op = "SentTrainingAmplifier";
break;
case Focus3Operation.UnbindUpgrade:
op = "UnbindUpgrade";
break;
case Focus3Operation.ConvertShard:
op = "ConvertShard";
break;
}
}
switch (op) {
const accountId = await getAccountIdForRequest(req);
switch (req.query.op) {
default:
logger.error("Unhandled focus op type: " + String(req.query.op));
logger.debug(String(req.body));
res.end();
break;
case "InstallLens": {
case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(account._id.toString());
const inventory = await getInventory(accountId);
const item = inventory[request.Category].id(request.WeaponId);
if (item) {
item.FocusLens = request.LensType;
@ -103,10 +35,10 @@ export const focusController: RequestHandler = async (req, res) => {
});
break;
}
case "UnlockWay": {
case FocusOperation.UnlockWay: {
const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
const focusPolarity = focusTypeToPolarity(focusType);
const inventory = await getInventory(account._id.toString(), "FocusAbility FocusUpgrades FocusXP");
const inventory = await getInventory(accountId, "FocusAbility FocusUpgrades FocusXP");
const cost = inventory.FocusAbility ? 50_000 : 0;
inventory.FocusAbility ??= focusType;
inventory.FocusUpgrades.push({ ItemType: focusType });
@ -120,29 +52,12 @@ export const focusController: RequestHandler = async (req, res) => {
});
break;
}
case "IncreasePool": {
const request = JSON.parse(String(req.body)) as IIncreasePoolRequest;
const focusPolarity = focusTypeToPolarity(request.FocusType);
const inventory = await getInventory(account._id.toString(), "FocusXP FocusCapacity");
let cost = 0;
for (let capacity = request.CurrentTotalCapacity; capacity != request.NewTotalCapacity; ++capacity) {
cost += increasePoolCost[capacity - 5];
}
inventory.FocusXP![focusPolarity]! -= cost;
inventory.FocusCapacity = request.NewTotalCapacity;
await inventory.save();
res.json({
TotalCapacity: request.NewTotalCapacity,
FocusPointCosts: { [focusPolarity]: cost }
});
break;
}
case "ActivateWay": {
case FocusOperation.ActivateWay: {
const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
await Inventory.updateOne(
{
accountOwnerId: account._id.toString()
accountOwnerId: accountId
},
{
FocusAbility: focusType
@ -154,20 +69,13 @@ export const focusController: RequestHandler = async (req, res) => {
});
break;
}
case "UnlockUpgrade": {
case FocusOperation.UnlockUpgrade: {
const request = JSON.parse(String(req.body)) as IUnlockUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(account._id.toString());
const inventory = await getInventory(accountId);
let cost = 0;
for (const focusType of request.FocusTypes) {
if (focusType in ExportFocusUpgrades) {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
} else if (focusType == "/Lotus/Upgrades/Focus/Power/Residual/ChannelEfficiencyFocusUpgrade") {
// Zenurik's Inner Might (Focus 2.0)
cost += 50_000;
} else {
logger.warn(`unknown focus upgrade ${focusType}, will unlock it for free`);
}
cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
}
inventory.FocusXP![focusPolarity]! -= cost;
@ -178,20 +86,15 @@ export const focusController: RequestHandler = async (req, res) => {
});
break;
}
case "LevelUpUpgrade":
case "UpdateUpgrade": {
case FocusOperation.LevelUpUpgrade: {
const request = JSON.parse(String(req.body)) as ILevelUpUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusInfos[0].ItemType);
const inventory = await getInventory(account._id.toString());
const inventory = await getInventory(accountId);
let cost = 0;
for (const focusUpgrade of request.FocusInfos) {
cost += focusUpgrade.FocusXpCost;
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
if (op == "UpdateUpgrade") {
focusUpgradeDb.IsActive = focusUpgrade.IsActive;
} else {
focusUpgradeDb.Level = focusUpgrade.Level;
}
focusUpgradeDb.Level = focusUpgrade.Level;
}
inventory.FocusXP![focusPolarity]! -= cost;
await inventory.save();
@ -201,9 +104,9 @@ export const focusController: RequestHandler = async (req, res) => {
});
break;
}
case "SentTrainingAmplifier": {
case FocusOperation.SentTrainingAmplifier: {
const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
const inventory = await getInventory(account._id.toString());
const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
@ -216,10 +119,10 @@ export const focusController: RequestHandler = async (req, res) => {
res.json(inventoryChanges.OperatorAmps![0]);
break;
}
case "UnbindUpgrade": {
case FocusOperation.UnbindUpgrade: {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(account._id.toString());
const inventory = await getInventory(accountId);
inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [
{
@ -246,7 +149,7 @@ export const focusController: RequestHandler = async (req, res) => {
});
break;
}
case "ConvertShard": {
case FocusOperation.ConvertShard: {
const request = JSON.parse(String(req.body)) as IConvertShardRequest;
// Tally XP
let xp = 0;
@ -264,7 +167,7 @@ export const focusController: RequestHandler = async (req, res) => {
for (const shard of request.Shards) {
shard.ItemCount *= -1;
}
const inventory = await getInventory(account._id.toString());
const inventory = await getInventory(accountId);
const polarity = request.Polarity;
inventory.FocusXP ??= {};
inventory.FocusXP[polarity] ??= 0;
@ -276,8 +179,7 @@ export const focusController: RequestHandler = async (req, res) => {
}
};
// Focus 3.0
enum Focus3Operation {
enum FocusOperation {
InstallLens = "1",
UnlockWay = "2",
UnlockUpgrade = "3",
@ -288,20 +190,6 @@ enum Focus3Operation {
ConvertShard = "9"
}
// Focus 2.0
enum Focus2Operation {
InstallLens = "1",
UnlockWay = "2",
UnlockUpgrade = "3",
IncreasePool = "4",
LevelUpUpgrade = "5",
ActivateWay = "6",
UpdateUpgrade = "7", // used to change the IsActive state, same format as ILevelUpUpgradeRequest
SentTrainingAmplifier = "9",
UnbindUpgrade = "10",
ConvertShard = "11"
}
// For UnlockWay & ActivateWay
interface IWayRequest {
FocusType: string;
@ -311,13 +199,6 @@ interface IUnlockUpgradeRequest {
FocusTypes: string[];
}
// Focus 2.0
interface IIncreasePoolRequest {
FocusType: string;
CurrentTotalCapacity: number;
NewTotalCapacity: number;
}
interface ILevelUpUpgradeRequest {
FocusInfos: {
ItemType: string;
@ -325,7 +206,6 @@ interface ILevelUpUpgradeRequest {
IsUniversal: boolean;
Level: number;
IsActiveAbility: boolean;
IsActive?: number; // Focus 2.0
}[];
}
@ -351,7 +231,7 @@ interface ILensInstallRequest {
// Works for ways & upgrades
const focusTypeToPolarity = (type: string): TFocusPolarity => {
return ("AP_" + type.substring(1).split("/")[3].toUpperCase()) as TFocusPolarity;
return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as TFocusPolarity;
};
const shardValues = {
@ -360,19 +240,3 @@ const shardValues = {
"/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem": 25_000,
"/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantTierTwoItem": 40_000
};
// Starting at a capacity of 5 (Source: https://wiki.warframe.com/w/Focus_2.0)
const increasePoolCost = [
2576, 3099, 3638, 4190, 4755, 5331, 5918, 6514, 7120, 7734, 8357, 8988, 9626, 10271, 10923, 11582, 12247, 12918,
13595, 14277, 14965, 15659, 16357, 17061, 17769, 18482, 19200, 19922, 20649, 21380, 22115, 22854, 23597, 24344,
25095, 25850, 26609, 27371, 28136, 28905, 29678, 30454, 31233, 32015, 32801, 33590, 34382, 35176, 35974, 36775,
37579, 38386, 39195, 40008, 40823, 41641, 42461, 43284, 44110, 44938, 45769, 46603, 47439, 48277, 49118, 49961,
50807, 51655, 52505, 53357, 54212, 55069, 55929, 56790, 57654, 58520, 59388, 60258, 61130, 62005, 62881, 63759,
64640, 65522, 66407, 67293, 68182, 69072, 69964, 70858, 71754, 72652, 73552, 74453, 75357, 76262, 77169, 78078,
78988, 79900, 80814, 81730, 82648, 83567, 84488, 85410, 86334, 87260, 88188, 89117, 90047, 90980, 91914, 92849,
93786, 94725, 95665, 96607, 97550, 98495, 99441, 100389, 101338, 102289, 103241, 104195, 105150, 106107, 107065,
108024, 108985, 109948, 110911, 111877, 112843, 113811, 114780, 115751, 116723, 117696, 118671, 119647, 120624,
121603, 122583, 123564, 124547, 125531, 126516, 127503, 128490, 129479, 130470, 131461, 132454, 133448, 134443,
135440, 136438, 137437, 138437, 139438, 140441, 141444, 142449, 143455, 144463, 145471, 146481, 147492, 148503,
149517
];

View File

@ -1,27 +0,0 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
export const forceRemoveItemController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "MiscItems");
const body = getJSONfromString<IForceRemoveItemRequest>(String(req.body));
const inventoryChanges: IInventoryChanges = {};
for (const item of body.items) {
const index = inventory.MiscItems.findIndex(x => x.ItemType == item);
if (index != -1) {
inventoryChanges.MiscItems ??= [];
inventoryChanges.MiscItems.push({ ItemType: item, ItemCount: inventory.MiscItems[index].ItemCount * -1 });
inventory.MiscItems.splice(index, 1);
}
}
await inventory.save();
res.json({ InventoryChanges: inventoryChanges });
};
interface IForceRemoveItemRequest {
items: string[];
}

View File

@ -19,7 +19,7 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
_id: new Types.ObjectId(),
pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
ppf: "",
CompletionTime: new Date(Date.now() - 1000),
CompletionTime: new Date(Date.now()),
DecoCapacity: 600
});
await guild.save();

View File

@ -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";

View File

@ -1,4 +1,6 @@
import type { RequestHandler } from "express";
import { config } from "../../services/configService.ts";
import allShipFeatures from "../../../static/fixed_responses/allShipFeatures.json" with { type: "json" };
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { IGetShipResponse, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
@ -29,5 +31,9 @@ export const getShipController: RequestHandler = async (req, res) => {
TailorShop: personalRooms.TailorShop
};
if (config.unlockAllShipFeatures) {
getShipResponse.Ship.Features = allShipFeatures;
}
res.json(getShipResponse);
};

View File

@ -11,11 +11,7 @@ export const getVoidProjectionRewardsController: RequestHandler = async (req, re
if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
const inventory = await getInventory(accountId);
const reward = await crackRelic(inventory, data.ParticipantInfo);
if (!inventory.MissionRelicRewards || inventory.MissionRelicRewards.length >= data.CurrentWave) {
inventory.MissionRelicRewards = [];
}
inventory.MissionRelicRewards.push({ ItemType: reward.type, ItemCount: reward.itemCount });
await crackRelic(inventory, data.ParticipantInfo);
await inventory.save();
}

View File

@ -1,6 +1,6 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
@ -75,5 +75,5 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods
});
broadcastInventoryUpdate(req);
sendWsBroadcastTo(accountId, { update_inventory: true });
};

View File

@ -1,17 +1,16 @@
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);
const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain)!;
const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo, questKey);
const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo);
await inventory.save();
res.send(inventoryChanges);

View File

@ -8,9 +8,8 @@ export const giveKeyChainTriggeredMessageController: RequestHandler = async (req
const accountId = await getAccountIdForRequest(req);
const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest;
const inventory = await getInventory(accountId, "QuestKeys accountOwnerId");
const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain)!;
await giveKeyChainMessage(inventory, keyChainInfo, questKey);
const inventory = await getInventory(accountId, "QuestKeys");
await giveKeyChainMessage(inventory, accountId, keyChainInfo);
await inventory.save();
res.send(1);

View File

@ -28,6 +28,7 @@ import {
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import { config } from "../../services/configService.ts";
import type { ITechProjectClient } from "../../types/guildTypes.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { GuildMember } from "../../models/guildModel.ts";
@ -82,16 +83,16 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.TechProjects[
guild.TechProjects.push({
ItemType: data.RecipeType,
ReqCredits: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})),
State: 0
}) - 1
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (guild.noDojoResearchCosts) {
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {

View File

@ -1,6 +1,7 @@
import type { RequestHandler } from "express";
import { config, getReflexiveAddress } from "../../services/configService.ts";
import { getReflexiveAddress } from "../../services/configService.ts";
export const hubController: RequestHandler = (req, res) => {
res.json(`hub ${config.hubAddress ?? getReflexiveAddress(req).myAddress}:6952`);
const { myAddress } = getReflexiveAddress(req);
res.json(`hub ${myAddress}:6952`);
};

View File

@ -1,13 +1,12 @@
import type { Request, RequestHandler } from "express";
import type { RequestHandler } from "express";
import { Inbox } from "../../models/inboxModel.ts";
import {
createMessage,
createNewEventMessages,
deleteAllMessagesRead,
deleteAllMessagesReadNonCin,
deleteMessageRead,
getAllMessagesSorted,
getMessage,
type IMessageCreationTemplate
getMessage
} from "../../services/inboxService.ts";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "../../services/loginService.ts";
import {
@ -22,9 +21,6 @@ import { ExportFlavour } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
import { fromStoreItem, isStoreItem } from "../../services/itemDataService.ts";
import type { IOid } from "../../types/commonTypes.ts";
import { unixTimesInMs } from "../../constants/timeConstants.ts";
import { config } from "../../services/configService.ts";
import { Types } from "mongoose";
export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
@ -35,11 +31,11 @@ export const inboxController: RequestHandler = async (req, res) => {
if (deleteId) {
if (deleteId === "DeleteAllRead") {
await deleteAllMessagesRead(accountId);
} else if (deleteId === "DeleteAllReadNonCin") {
await deleteAllMessagesReadNonCin(accountId);
} else {
await deleteMessageRead(parseOid(deleteId as string));
res.status(200).end();
return;
}
await deleteMessageRead(parseOid(deleteId as string));
res.status(200).end();
} else if (messageId) {
const message = await getMessage(parseOid(messageId as string));
@ -138,119 +134,6 @@ export const inboxController: RequestHandler = async (req, res) => {
}
};
const createNewEventMessages = async (req: Request): Promise<void> => {
const account = await getAccountForRequest(req);
const newEventMessages: IMessageCreationTemplate[] = [];
// Baro
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12);
if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) {
newEventMessages.push({
sndr: "/Lotus/Language/G1Quests/VoidTraderName",
sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle",
msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage",
icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png",
startDate: new Date(baroActualStart),
endDate: new Date(baroStart + unixTimesInMs.day * 14),
CrossPlatform: true,
arg: [
{
Key: "NODE_NAME",
Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4]
}
],
date: new Date(baroActualStart)
});
}
// BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events.
const promises = [];
if (config.worldState?.creditBoost) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) {
newEventMessages.push({
globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"),
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
sub: "/Lotus/Language/Items/EventDoubleCreditsName",
msg: "/Lotus/Language/Items/EventDoubleCreditsDesc",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
startDate: new Date(),
CrossPlatform: true
});
}
})()
);
}
if (config.worldState?.affinityBoost) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) {
newEventMessages.push({
globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"),
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
sub: "/Lotus/Language/Items/EventDoubleAffinityName",
msg: "/Lotus/Language/Items/EventDoubleAffinityDesc",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
startDate: new Date(),
CrossPlatform: true
});
}
})()
);
}
if (config.worldState?.resourceBoost) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) {
newEventMessages.push({
globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"),
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
sub: "/Lotus/Language/Items/EventDoubleResourceName",
msg: "/Lotus/Language/Items/EventDoubleResourceDesc",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
startDate: new Date(),
CrossPlatform: true
});
}
})()
);
}
if (config.worldState?.galleonOfGhouls) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
newEventMessages.push({
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
startDate: new Date(),
goalTag: "GalleonRobbery"
});
}
})()
);
}
await Promise.all(promises);
if (newEventMessages.length === 0) {
return;
}
await createMessage(account._id, newEventMessages);
const latestEventMessage = newEventMessages.reduce((prev, current) =>
prev.startDate! > current.startDate! ? prev : current
);
account.LatestEventMessageDate = new Date(latestEventMessage.startDate!);
await account.save();
};
// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb
const parseOid = (oid: string): string => {
if (oid[0] == "{") {

View File

@ -10,7 +10,7 @@ import type {
IMiscItem
} from "../../types/inventoryTypes/inventoryTypes.ts";
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { ExportResources } from "warframe-public-export-plus";
import { ExportMisc } from "warframe-public-export-plus";
import { getRecipe } from "../../services/itemDataService.ts";
import { toMongoDate, version_compare } from "../../helpers/inventoryHelpers.ts";
import { logger } from "../../utils/logger.ts";
@ -20,7 +20,6 @@ import {
applyCheatsToInfestedFoundry,
handleSubsumeCompletion
} from "../../services/infestedFoundryService.ts";
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
export const infestedFoundryController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
@ -146,7 +145,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const currentUnixSeconds = Math.trunc(Date.now() / 1000);
for (const contribution of request.ResourceContributions) {
const snack = ExportResources[contribution.ItemType].helminthSnack!;
const snack = ExportMisc.helminthSnacks[contribution.ItemType];
// tally items for removal
const change = miscItemChanges.find(x => x.ItemType == contribution.ItemType);
@ -364,7 +363,6 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
);
addRecipes(inventory, recipeChanges);
await inventory.save();
sendWsBroadcastToGame(account._id.toString(), { sync_inventory: true });
}
res.end();
break;

View File

@ -10,7 +10,7 @@ import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import type { ICountedItem } from "warframe-public-export-plus";
import { ExportCustoms } from "warframe-public-export-plus";
import { eFaction, ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "../../services/infestedFoundryService.ts";
import {
addEmailItem,
@ -177,7 +177,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
}
await cleanupInventory(inventory);
cleanupInventory(inventory);
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
//await inventory.save();
@ -220,10 +220,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
await createMessage(account._id, [
{
sndr:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Language/Menu/GrineerInvasionLeader"
: "/Lotus/Language/Menu/CorpusInvasionLeader",
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
@ -321,7 +318,21 @@ export const getInventoryResponse = async (
}
}
if (config.worldState?.baroTennoConRelay) {
if (config.unlockAllShipDecorations) {
inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
}
}
}
if (config.unlockAllFlavourItems) {
inventoryResponse.FlavourItems = [];
for (const uniqueName in ExportFlavour) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
} else if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess",
@ -335,29 +346,39 @@ export const getInventoryResponse = async (
}
if (config.unlockAllSkins) {
const ownedWeaponSkins = new Set<string>(inventoryResponse.WeaponSkins.map(x => x.ItemType));
for (const [uniqueName, meta] of Object.entries(ExportCustoms)) {
if (!meta.alwaysAvailable && !ownedWeaponSkins.has(uniqueName)) {
inventoryResponse.WeaponSkins.push({
ItemId: {
$oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0")
},
ItemType: uniqueName
const missingWeaponSkins = new Set(Object.keys(ExportCustoms));
inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType));
for (const uniqueName of missingWeaponSkins) {
inventoryResponse.WeaponSkins.push({
ItemId: {
$oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0")
},
ItemType: uniqueName
});
}
}
if (config.unlockAllCapturaScenes) {
for (const uniqueName of Object.keys(ExportResources)) {
if (resourceInheritsFrom(uniqueName, "/Lotus/Types/Items/MiscItems/PhotoboothTile")) {
inventoryResponse.MiscItems.push({
ItemType: uniqueName,
ItemCount: 1
});
}
}
}
if (inventory.spoofMasteryRank && inventory.spoofMasteryRank >= 0) {
inventoryResponse.PlayerLevel = inventory.spoofMasteryRank;
if (typeof config.spoofMasteryRank === "number" && config.spoofMasteryRank >= 0) {
inventoryResponse.PlayerLevel = config.spoofMasteryRank;
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(config.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
});
}
}
@ -461,9 +482,6 @@ export const getInventoryResponse = async (
toLegacyOid(id);
}
}
if (inventoryResponse.GuildId) {
toLegacyOid(inventoryResponse.GuildId);
}
}
}
}
@ -477,3 +495,21 @@ const getExpRequiredForMr = (rank: number): number => {
}
return 2_250_000 + 147_500 * (rank - 30);
};
const resourceInheritsFrom = (resourceName: string, targetName: string): boolean => {
let parentName = resourceGetParent(resourceName);
for (; parentName != undefined; parentName = resourceGetParent(parentName)) {
if (parentName == targetName) {
return true;
}
}
return false;
};
const resourceGetParent = (resourceName: string): string | undefined => {
if (resourceName in ExportResources) {
return ExportResources[resourceName].parentName;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName;
};

View File

@ -8,10 +8,7 @@ import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } f
import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../types/loginTypes.ts";
import { logger } from "../../utils/logger.ts";
import { version_compare } from "../../helpers/inventoryHelpers.ts";
import { handleNonceInvalidation } from "../../services/wsService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { createMessage } from "../../services/inboxService.ts";
import { fromStoreItem } from "../../services/itemDataService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
@ -77,25 +74,7 @@ export const loginController: RequestHandler = async (request, response) => {
account.LastLogin = new Date();
await account.save();
handleNonceInvalidation(account._id.toString());
// If the client crashed during an endless fissure mission, discharge rewards to an inbox message. (https://www.reddit.com/r/Warframe/comments/5uwwjm/til_if_you_crash_during_a_fissure_you_keep_any/)
const inventory = await getInventory(account._id.toString(), "MissionRelicRewards");
if (inventory.MissionRelicRewards) {
await createMessage(account._id, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Menu/VoidProjectionItemsMessage",
sub: "/Lotus/Language/Menu/VoidProjectionItemsSubject",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
countedAtt: inventory.MissionRelicRewards.map(x => ({ ...x, ItemType: fromStoreItem(x.ItemType) })),
attVisualOnly: true,
highPriority: true // TOVERIFY
}
]);
inventory.MissionRelicRewards = undefined;
await inventory.save();
}
sendWsBroadcastTo(account._id.toString(), { nonce_updated: true });
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
};
@ -116,11 +95,11 @@ const createLoginResponse = (
BuildLabel: buildLabel
};
if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) {
resp.NRS = [config.nrsAddress ?? myAddress];
resp.NRS = [myAddress];
}
if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) {
// U17 and up
resp.IRC = [config.ircAddress ?? myAddress];
resp.IRC = config.myIrcAddresses ?? [myAddress];
}
if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) {
// U24 and up

View File

@ -1,6 +1,6 @@
import type { RequestHandler } from "express";
import { Account } from "../../models/loginModel.ts";
import { handleNonceInvalidation } from "../../services/wsService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
export const logoutController: RequestHandler = async (req, res) => {
if (!req.query.accountId) {
@ -21,7 +21,7 @@ export const logoutController: RequestHandler = async (req, res) => {
}
);
if (stat.modifiedCount) {
handleNonceInvalidation(req.query.accountId as string);
sendWsBroadcastTo(req.query.accountId as string, { nonce_updated: true });
}
res.writeHead(200, {

View File

@ -2,7 +2,6 @@ import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const maturePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -20,7 +19,6 @@ export const maturePetController: RequestHandler = async (req, res) => {
: [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern],
unmature: data.revert
});
broadcastInventoryUpdate(req);
};
interface IMaturePetRequest {

View File

@ -2,11 +2,7 @@ import type { RequestHandler } from "express";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import type { IMissionInventoryUpdateRequest } from "../../types/requestTypes.ts";
import {
addMissionInventoryUpdates,
addMissionRewards,
handleConservation
} from "../../services/missionInventoryUpdateService.ts";
import { addMissionInventoryUpdates, addMissionRewards } from "../../services/missionInventoryUpdateService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getInventoryResponse } from "./inventoryController.ts";
import { logger } from "../../utils/logger.ts";
@ -98,7 +94,6 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(account, inventory, missionReport, firstCompletion);
handleConservation(inventory, missionReport, AffiliationMods); // Conservation reports have GS_SUCCESS
if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed();
@ -116,35 +111,19 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
AffiliationMods,
ConquestCompletedMissionsCount
};
if (
missionReport.BMI ||
missionReport.TNT ||
missionReport.SSC ||
missionReport.RJ ||
missionReport.SS ||
missionReport.CMI ||
missionReport.EJC
) {
logger.debug(`interstitial request, sending only deltas`, deltas);
if (missionReport.RJ) {
logger.debug(`railjack interstitial request, sending only deltas`, deltas);
res.json(deltas);
} else if (missionReport.RewardInfo) {
logger.debug(`classic mission completion, sending everything`);
const inventoryResponse = await getInventoryResponse(
inventory,
"xpBasedLevelCapDisabled" in req.query,
account.BuildLabel
);
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
res.json({
InventoryJson: JSON.stringify(inventoryResponse),
...deltas
} satisfies IMissionInventoryUpdateResponse);
} else {
logger.debug(`no reward info, assuming this wasn't a mission completion and we should just sync inventory`);
const inventoryResponse = await getInventoryResponse(
inventory,
"xpBasedLevelCapDisabled" in req.query,
account.BuildLabel
);
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
res.json({
InventoryJson: JSON.stringify(inventoryResponse)
} satisfies IMissionInventoryUpdateResponseBackToDryDock);

View File

@ -1,6 +1,6 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import {
getInventory,
@ -197,5 +197,5 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
MiscItems: miscItemChanges
}
});
broadcastInventoryUpdate(req);
sendWsBroadcastTo(accountId, { update_inventory: true });
};

View File

@ -3,7 +3,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
interface INameWeaponRequest {
ItemName: string;
@ -28,5 +28,5 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
res.json({
InventoryChanges: currencyChanges
});
broadcastInventoryUpdate(req);
sendWsBroadcastTo(accountId, { update_inventory: true });
};

View File

@ -1,4 +1,4 @@
import { fromDbOid, toMongoDate, version_compare } from "../../helpers/inventoryHelpers.ts";
import { fromDbOid, version_compare } from "../../helpers/inventoryHelpers.ts";
import type { IKnifeResponse } from "../../helpers/nemesisHelpers.ts";
import {
antivirusMods,
@ -149,10 +149,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
break;
}
}
const antivirusGainMultiplier = (
await getInventory(account._id.toString(), "nemesisAntivirusGainMultiplier")
).nemesisAntivirusGainMultiplier;
inventory.Nemesis!.HenchmenKilled += antivirusGain * (antivirusGainMultiplier ?? 1);
inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
@ -310,26 +307,6 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({
target: inventory.toJSON().Nemesis
});
} else if ((req.query.mode as string) == "t") {
const inventory = await getInventory(account._id.toString(), "LastNemesisAllySpawnTime");
//const body = getJSONfromString<IUpdateAllySpawnTimeRequest>(String(req.body));
const now = new Date(Math.trunc(Date.now() / 1000) * 1000);
inventory.LastNemesisAllySpawnTime = now;
await inventory.save();
res.json({
NewTime: toMongoDate(now)
} satisfies IUpdateAllySpawnTimeResponse);
} else if ((req.query.mode as string) == "d") {
const inventory = await getInventory(account._id.toString(), "NemesisHistory");
const body = getJSONfromString<IRelinquishAdversariesRequest>(String(req.body));
for (const fp of body.nemesisFingerprints) {
const index = inventory.NemesisHistory!.findIndex(x => x.fp == fp);
if (index != -1) {
inventory.NemesisHistory!.splice(index, 1);
}
}
await inventory.save();
res.json(body);
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(account._id.toString(), "Nemesis");
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
@ -467,15 +444,3 @@ const consumeModCharge = (
response.UpgradeNew.push(true);
}
};
interface IRelinquishAdversariesRequest {
nemesisFingerprints: (bigint | number)[];
}
// interface IUpdateAllySpawnTimeRequest {
// LastSpawnTime: IMongoDate;
// }
interface IUpdateAllySpawnTimeResponse {
NewTime: IMongoDate;
}

View File

@ -13,6 +13,7 @@ import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
import { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
import { config } from "../../services/configService.ts";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -73,7 +74,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
}
}
if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || guild.noDojoDecoBuildStage) {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
deco.CompletionTime = new Date();
if (meta) {
processDojoBuildMaterialsGathered(guild, meta);

View File

@ -1,3 +1,4 @@
import { config } from "../../services/configService.ts";
import {
getDojoClient,
getGuildForRequestEx,
@ -20,7 +21,7 @@ export const queueDojoComponentDestructionController: RequestHandler = async (re
const componentId = req.query.componentId as string;
guild.DojoComponents.id(componentId)!.DestructionTime = new Date(
(Math.trunc(Date.now() / 1000) + (guild.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000
(Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000
);
await guild.save();

View File

@ -1,7 +1,7 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import type { RequestHandler } from "express";
export const releasePetController: RequestHandler = async (req, res) => {
@ -20,7 +20,7 @@ export const releasePetController: RequestHandler = async (req, res) => {
await inventory.save();
res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
broadcastInventoryUpdate(req);
sendWsBroadcastTo(accountId, { update_inventory: true });
};
interface IReleasePetRequest {

View File

@ -10,7 +10,6 @@ import {
import { createMessage } from "../../services/inboxService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
@ -86,7 +85,6 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
ItemToRemove: "/Lotus/Types/Keys/DojoKey",
RecipeToRemove: "/Lotus/Types/Keys/DojoKeyBlueprint"
});
sendWsBroadcastTo(payload.userId, { update_inventory: true });
};
interface IRemoveFromGuildRequest {

View File

@ -1,7 +1,7 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { RequestHandler } from "express";
@ -23,7 +23,7 @@ export const renamePetController: RequestHandler = async (req, res) => {
...data,
inventoryChanges: inventoryChanges
});
broadcastInventoryUpdate(req);
sendWsBroadcastTo(accountId, { update_inventory: true });
};
interface IRenamePetRequest {

View File

@ -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;
};

View File

@ -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();
};

View File

@ -17,7 +17,7 @@ import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { sendWsBroadcastEx } from "../../services/wsService.ts";
import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts";
export const sellController: RequestHandler = async (req, res) => {
@ -77,9 +77,6 @@ export const sellController: RequestHandler = async (req, res) => {
requiredFields.add("CrewShipSalvagedWeaponSkins");
}
}
if (payload.Items.WeaponSkins) {
requiredFields.add("WeaponSkins");
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
// Give currency
@ -305,17 +302,12 @@ export const sellController: RequestHandler = async (req, res) => {
addFusionTreasures(inventory, [parseFusionTreasure(sellItem.String, sellItem.Count * -1)]);
});
}
if (payload.Items.WeaponSkins) {
payload.Items.WeaponSkins.forEach(sellItem => {
inventory.WeaponSkins.pull({ _id: sellItem.String });
});
}
await inventory.save();
res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
});
broadcastInventoryUpdate(req);
sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
};
interface ISellRequest {
@ -343,7 +335,6 @@ interface ISellRequest {
CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[];
FusionTreasures?: ISellItem[];
WeaponSkins?: ISellItem[]; // SNS specific field
};
SellPrice: number;
SellCurrency:

View File

@ -57,7 +57,7 @@ export const setGuildMotdController: RequestHandler = async (req, res) => {
await guild.save();
}
if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.11.04.18.58") > 0) {
if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.03.24.20.24") > 0) {
res.json({ IsLongMOTD, MOTD });
} else {
res.send(MOTD).end();

View File

@ -1,7 +1,6 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const setSupportedSyndicateController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -16,5 +15,4 @@ export const setSupportedSyndicateController: RequestHandler = async (req, res)
);
res.end();
broadcastInventoryUpdate(req);
};

View File

@ -17,7 +17,7 @@ export const startCollectibleEntryController: RequestHandler = async (req, res)
IncentiveStates: request.other
});
await inventory.save();
res.send(`target = ${request.target}key = 0key = 1{"Target":"${request.target}"}`);
res.status(200).end();
};
interface IStartCollectibleEntryRequest {

View File

@ -11,9 +11,9 @@ import {
} from "../../services/guildService.ts";
import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { config } from "../../services/configService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { fromOid } from "../../helpers/inventoryHelpers.ts";
interface IStartDojoRecipeRequest {
PlacedComponent: IDojoComponentClient;
@ -51,13 +51,13 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
_id: componentId,
pf: request.PlacedComponent.pf,
ppf: request.PlacedComponent.ppf,
pi: new Types.ObjectId(fromOid(request.PlacedComponent.pi!)),
pi: new Types.ObjectId(request.PlacedComponent.pi!.$oid),
op: request.PlacedComponent.op,
pp: request.PlacedComponent.pp,
DecoCapacity: room?.decoCapacity
}) - 1
];
if (guild.noDojoRoomBuildStage) {
if (config.noDojoRoomBuildStage) {
component.CompletionTime = new Date(Date.now());
if (room) {
processDojoBuildMaterialsGathered(guild, room);

View File

@ -1,20 +1,11 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { unlockShipFeature } from "../../services/personalRoomsService.ts";
import { updateShipFeature } from "../../services/personalRoomsService.ts";
import type { IUnlockShipFeatureRequest } from "../../types/requestTypes.ts";
import { parseString } from "../../helpers/general.ts";
export const unlockShipFeatureController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "MiscItems accountOwnerId");
const request = getJSONfromString<IUnlockShipFeatureRequest>(String(req.body));
await unlockShipFeature(inventory, request.Feature);
await inventory.save();
const accountId = parseString(req.query.accountId);
const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest;
await updateShipFeature(accountId, shipFeatureRequest.Feature);
res.send([]);
};
interface IUnlockShipFeatureRequest {
Feature: string;
KeyChain: string;
ChainStage: number;
}

View File

@ -14,7 +14,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
const inventory = await getInventory(
account._id.toString(),
"ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress nightwaveStandingMultiplier"
"ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress"
);
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) {

View File

@ -5,7 +5,6 @@ import type { IUpdateQuestRequest } from "../../services/questService.ts";
import { updateQuestKey } from "../../services/questService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
export const updateQuestController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
@ -30,5 +29,4 @@ export const updateQuestController: RequestHandler = async (req, res) => {
await inventory.save();
res.send(updateQuestResponse);
sendWsBroadcastTo(accountId, { update_inventory: true });
};

View File

@ -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": {

View File

@ -1,6 +1,5 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
@ -20,7 +19,6 @@ export const abilityOverrideController: RequestHandler = async (req, res) => {
}
}
res.end();
broadcastInventoryUpdate(req);
};
interface IAbilityOverrideRequest {

View File

@ -1,42 +1,21 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const addCurrencyController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IAddCurrencyRequest;
let projection = request.currency as string;
if (request.currency.startsWith("Vault")) projection = "GuildId";
const inventory = await getInventory(accountId, projection);
const inventory = await getInventory(accountId, request.currency);
if (request.currency == "FusionPoints") {
addFusionPoints(inventory, request.delta);
} else if (request.currency == "VaultRegularCredits" || request.currency == "VaultPremiumCredits") {
const guild = await getGuildForRequestEx(req, inventory);
if (await hasGuildPermission(guild, accountId, GuildPermission.Treasurer)) {
guild[request.currency] ??= 0;
guild[request.currency]! += request.delta;
await guild.save();
}
} else {
inventory[request.currency] += request.delta;
}
if (!request.currency.startsWith("Vault")) {
await inventory.save();
broadcastInventoryUpdate(req);
}
await inventory.save();
res.end();
};
interface IAddCurrencyRequest {
currency:
| "RegularCredits"
| "PremiumCredits"
| "FusionPoints"
| "PrimeTokens"
| "VaultRegularCredits"
| "VaultPremiumCredits";
currency: "RegularCredits" | "PremiumCredits" | "FusionPoints" | "PrimeTokens";
delta: number;
}

View File

@ -1,7 +1,6 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory, addItem } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const addItemsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -12,7 +11,6 @@ export const addItemsController: RequestHandler = async (req, res) => {
}
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};
interface IAddItemRequest {

View File

@ -2,7 +2,6 @@ import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory, addRecipes } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { ExportRecipes } from "warframe-public-export-plus";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const addMissingHelminthBlueprintsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -22,5 +21,4 @@ export const addMissingHelminthBlueprintsController: RequestHandler = async (req
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};

View File

@ -2,7 +2,6 @@ import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const addMissingMaxRankModsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -42,5 +41,4 @@ export const addMissingMaxRankModsController: RequestHandler = async (req, res)
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};

View File

@ -1,43 +0,0 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { ITypeCount } from "../../types/commonTypes.ts";
export const addVaultTypeCountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const { vaultType, items } = req.body as {
vaultType: keyof typeof vaultConfig;
items: ITypeCount[];
};
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, vaultConfig[vaultType]))) {
res.status(400).send("-1").end();
return;
}
guild[vaultType] ??= [];
for (const item of items) {
const index = guild[vaultType].findIndex(x => x.ItemType === item.ItemType);
if (index === -1) {
guild[vaultType].push({
ItemType: item.ItemType,
ItemCount: item.ItemCount
});
} else {
guild[vaultType][index].ItemCount += item.ItemCount;
if (guild[vaultType][index].ItemCount < 1) {
guild[vaultType].splice(index, 1);
}
}
}
await guild.save();
res.end();
};
const vaultConfig = {
VaultShipDecorations: GuildPermission.Treasurer,
VaultMiscItems: GuildPermission.Treasurer,
VaultDecoRecipes: GuildPermission.Architect
} as const;

View File

@ -1,11 +1,10 @@
import { applyClientEquipmentUpdates, getInventory } from "../../services/inventoryService.ts";
import { getMaxLevelCap } from "../../services/itemDataService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import type { IOid } from "../../types/commonTypes.ts";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
import { ExportMisc } from "warframe-public-export-plus";
export const addXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -15,7 +14,7 @@ export const addXpController: RequestHandler = async (req, res) => {
for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid);
if (dbItem) {
if (getMaxLevelCap(dbItem.ItemType) > 30) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) {
dbItem.Polarized = 5;
}
@ -26,7 +25,6 @@ export const addXpController: RequestHandler = async (req, res) => {
}
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};
type IAddXpRequest = {

View File

@ -1,6 +1,5 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
@ -21,7 +20,6 @@ export const changeModularPartsController: RequestHandler = async (req, res) =>
await inventory.save();
}
res.end();
broadcastInventoryUpdate(req);
};
interface IUpdateFingerPrintRequest {

View File

@ -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();
};

View File

@ -1,5 +1,5 @@
import type { RequestHandler } from "express";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { Account, Ignore } from "../../models/loginModel.ts";
import { Inbox } from "../../models/inboxModel.ts";
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
@ -12,44 +12,33 @@ import { Leaderboard } from "../../models/leaderboardModel.ts";
import { deleteGuild } from "../../services/guildService.ts";
import { Friendship } from "../../models/friendModel.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import { config } from "../../services/configService.ts";
import { saveConfig } from "../../services/configWriterService.ts";
export const deleteAccountController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
// If this account is an admin, remove it from administratorNames
if (config.administratorNames) {
const adminIndex = config.administratorNames.indexOf(account.DisplayName);
if (adminIndex != -1) {
config.administratorNames.splice(adminIndex, 1);
await saveConfig();
}
}
const accountId = await getAccountIdForRequest(req);
// If account is the founding warlord of a guild, delete that guild as well.
const guildMember = await GuildMember.findOne({ accountId: account._id, rank: 0, status: 0 });
const guildMember = await GuildMember.findOne({ accountId, rank: 0, status: 0 });
if (guildMember) {
await deleteGuild(guildMember.guildId);
}
await Promise.all([
Account.deleteOne({ _id: account._id }),
Friendship.deleteMany({ owner: account._id }),
Friendship.deleteMany({ friend: account._id }),
GuildMember.deleteMany({ accountId: account._id }),
Ignore.deleteMany({ ignorer: account._id }),
Ignore.deleteMany({ ignoree: account._id }),
Inbox.deleteMany({ ownerId: account._id }),
Inventory.deleteOne({ accountOwnerId: account._id }),
Leaderboard.deleteMany({ ownerId: account._id }),
Loadout.deleteOne({ loadoutOwnerId: account._id }),
PersonalRooms.deleteOne({ personalRoomsOwnerId: account._id }),
Ship.deleteMany({ ShipOwnerId: account._id }),
Stats.deleteOne({ accountOwnerId: account._id })
Account.deleteOne({ _id: accountId }),
Friendship.deleteMany({ owner: accountId }),
Friendship.deleteMany({ friend: accountId }),
GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }),
Inbox.deleteMany({ ownerId: accountId }),
Inventory.deleteOne({ accountOwnerId: accountId }),
Leaderboard.deleteMany({ ownerId: accountId }),
Loadout.deleteOne({ loadoutOwnerId: accountId }),
PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }),
Ship.deleteMany({ ShipOwnerId: accountId }),
Stats.deleteOne({ accountOwnerId: accountId })
]);
sendWsBroadcastTo(account._id.toString(), { logged_out: true });
sendWsBroadcastTo(accountId, { logged_out: true });
res.end();
};

View File

@ -0,0 +1,34 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
const DEFAULT_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
export const editSuitInvigorationUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const { oid, data } = req.body as {
oid: string;
data?: {
DefensiveUpgrade: string;
OffensiveUpgrade: string;
UpgradesExpiry?: number;
};
};
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(oid)!;
if (data) {
suit.DefensiveUpgrade = data.DefensiveUpgrade;
suit.OffensiveUpgrade = data.OffensiveUpgrade;
if (data.UpgradesExpiry) {
suit.UpgradesExpiry = new Date(data.UpgradesExpiry);
} else {
suit.UpgradesExpiry = new Date(Date.now() + DEFAULT_UPGRADE_EXPIRY_MS);
}
} else {
suit.DefensiveUpgrade = undefined;
suit.OffensiveUpgrade = undefined;
suit.UpgradesExpiry = undefined;
}
await inventory.save();
res.end();
};

View File

@ -1,16 +0,0 @@
import { Alliance, Guild } from "../../models/guildModel.ts";
import { getAllianceClient } from "../../services/guildService.ts";
import type { RequestHandler } from "express";
export const getAllianceController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId;
if (guildId) {
const guild = await Guild.findById(guildId, "Name Tier AllianceId");
if (guild && guild.AllianceId) {
const alliance = (await Alliance.findById(guild.AllianceId))!;
res.json(await getAllianceClient(alliance, guild));
return;
}
}
res.end();
};

View File

@ -1,40 +0,0 @@
import type { RequestHandler } from "express";
import { Guild, GuildMember } from "../../models/guildModel.ts";
import { toMongoDate, toOid2 } from "../../helpers/inventoryHelpers.ts";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts";
import type { IGuildMemberClient } from "../../types/guildTypes.ts";
export const getGuildController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId;
if (guildId) {
const guild = await Guild.findById(guildId);
if (guild) {
const guildMembers = await GuildMember.find({ guildId: guild._id });
const members: IGuildMemberClient[] = [];
const dataFillInPromises: Promise<void>[] = [];
for (const guildMember of guildMembers) {
const member: IGuildMemberClient = {
_id: toOid2(guildMember.accountId, undefined),
Rank: guildMember.rank,
Status: guildMember.status,
Note: guildMember.RequestMsg,
RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined
};
dataFillInPromises.push(addAccountDataToFriendInfo(member));
dataFillInPromises.push(addInventoryDataToFriendInfo(member));
members.push(member);
}
await Promise.all(dataFillInPromises);
res.json({
...guild.toObject(),
Members: members
});
} else {
res.status(400).end();
}
}
};

View File

@ -7,12 +7,10 @@ import {
ExportAvionics,
ExportBoosters,
ExportCustoms,
ExportDojoRecipes,
ExportDrones,
ExportFactions,
ExportFlavour,
ExportGear,
ExportKeys,
ExportMisc,
ExportRailjackWeapons,
ExportRecipes,
ExportRelics,
@ -36,12 +34,10 @@ interface ListedItem {
partType?: string;
chainLength?: number;
parazon?: boolean;
alwaysAvailable?: boolean;
maxLevelCap?: number;
eligibleForVault?: boolean;
}
interface ItemLists {
uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[];
LongGuns: ListedItem[];
Melee: ListedItem[];
@ -63,28 +59,24 @@ interface ItemLists {
Boosters: ListedItem[];
VarziaOffers: ListedItem[];
Abilities: ListedItem[];
TechProjects: ListedItem[];
VaultDecoRecipes: ListedItem[];
FlavourItems: ListedItem[];
ShipDecorations: ListedItem[];
WeaponSkins: ListedItem[];
//circuitGameModes: ListedItem[];
}
const relicQualitySuffixes: Record<TRelicQuality, string> = {
VPQ_BRONZE: "",
VPQ_SILVER: "/Lotus/Language/Relics/VoidProjectionQuality_Silver",
VPQ_GOLD: "/Lotus/Language/Relics/VoidProjectionQuality_Gold",
VPQ_PLATINUM: "/Lotus/Language/Relics/VoidProjectionQuality_Platinum"
VPQ_SILVER: " [Exceptional]",
VPQ_GOLD: " [Flawless]",
VPQ_PLATINUM: " [Radiant]"
};
const toTitleCase = (str: string): string => {
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substring(1).toLowerCase());
};
/*const toTitleCase = (str: string): string => {
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase());
};*/
const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = {
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [],
LongGuns: [],
Melee: [],
@ -105,12 +97,7 @@ const getItemListsController: RequestHandler = (req, response) => {
mods: [],
Boosters: [],
VarziaOffers: [],
Abilities: [],
TechProjects: [],
VaultDecoRecipes: [],
FlavourItems: [],
ShipDecorations: [],
WeaponSkins: []
Abilities: []
/*circuitGameModes: [
{
uniqueName: "Survival",
@ -142,18 +129,11 @@ const getItemListsController: RequestHandler = (req, response) => {
}
]*/
};
const eligibleForVault = new Set<string>([
...Object.values(ExportDojoRecipes.research).flatMap(r => r.ingredients.map(i => i.ItemType)),
...Object.values(ExportDojoRecipes.fabrications).flatMap(f => f.ingredients.map(i => i.ItemType)),
...Object.values(ExportDojoRecipes.rooms).flatMap(r => r.ingredients.map(i => i.ItemType)),
...Object.values(ExportDojoRecipes.decos).flatMap(d => d.ingredients.map(i => i.ItemType))
]);
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
exalted: item.exalted,
maxLevelCap: item.maxLevelCap
exalted: item.exalted
});
item.abilities.forEach(ability => {
res.Abilities.push({
@ -201,8 +181,7 @@ const getItemListsController: RequestHandler = (req, response) => {
) {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
maxLevelCap: item.maxLevelCap
name: getString(item.name, lang)
});
}
} else if (!item.excludeFromCodex) {
@ -240,38 +219,27 @@ const getItemListsController: RequestHandler = (req, response) => {
}
}
}
if (item.productCategory == "ShipDecorations") {
res.ShipDecorations.push({
uniqueName: uniqueName,
name: name
});
} else if (
if (
name &&
uniqueName.substring(0, 30) != "/Lotus/Types/Game/Projections/" &&
uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" &&
uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle"
) {
res.miscitems.push({
uniqueName: uniqueName,
name: name,
subtype: "Resource",
...(eligibleForVault.has(uniqueName) && { eligibleForVault: true })
subtype: "Resource"
});
}
}
for (const [uniqueName, item] of Object.entries(ExportRelics)) {
const qualitySuffix =
item.quality !== "VPQ_BRONZE"
? ` [${toTitleCase(getString(relicQualitySuffixes[item.quality], lang))}]`
: "";
res.miscitems.push({
uniqueName: uniqueName,
name:
getString("/Lotus/Language/Relics/VoidProjectionName", lang)
.split("|ERA|")
.join(getString(`/Lotus/Language/Relics/Era_${item.era.toUpperCase()}`, lang))
.join(item.era)
.split("|CATEGORY|")
.join(item.category) + qualitySuffix
.join(item.category) + relicQualitySuffixes[item.quality]
});
}
for (const [uniqueName, item] of Object.entries(ExportGear)) {
@ -308,18 +276,10 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const [uniqueName, item] of Object.entries(ExportCustoms)) {
if (
item.productCategory == "WeaponSkins" &&
!uniqueName.startsWith("/Lotus/Types/Game/Lotus") && // Base Items
!uniqueName.endsWith("ProjectileSkin") && // UnrealTournament ProjectileSkins
!uniqueName.endsWith("Coat") // Frost Prime stuff
) {
res.WeaponSkins.push({
uniqueName: uniqueName,
name: getString(item.name, lang),
alwaysAvailable: item.alwaysAvailable
});
}
res.miscitems.push({
uniqueName: uniqueName,
name: getString(item.name, lang)
});
}
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
@ -353,7 +313,7 @@ const getItemListsController: RequestHandler = (req, response) => {
uniqueName,
name: getString(arcane.name, lang)
};
if (arcane.excludeFromCodex) {
if (arcane.isFrivolous) {
mod.badReason = "frivolous";
}
res.mods.push(mod);
@ -407,74 +367,6 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const uniqueName of Object.keys(ExportDojoRecipes.research)) {
if (
!["Zekti", "Vidar", "Lavan"].some(house => uniqueName.includes(house)) &&
!uniqueName.startsWith("/Lotus/Types/Items/ShipFeatureItems/Railjack/")
) {
let resultType;
if (uniqueName in ExportRecipes) {
resultType = ExportRecipes[uniqueName].resultType;
} else if (uniqueName in ExportDojoRecipes.fabrications) {
resultType = ExportDojoRecipes.fabrications[uniqueName].resultType;
} else if (uniqueName.startsWith("/Lotus/Types/Game/")) {
resultType = uniqueName.replace("Blueprint", "");
} else {
resultType = uniqueName;
}
let name = getString(getItemName(resultType) || resultType, lang);
if (uniqueName in ExportRecipes) {
const recipeNum = ExportRecipes[uniqueName].num;
if (recipeNum > 1) {
name = `${name} X ${recipeNum}`;
}
}
res.TechProjects.push({
uniqueName,
name
});
}
}
for (const uniqueName of [
...Object.entries(ExportDojoRecipes.decos)
.filter(([_, data]) => data.requiredInVault)
.map(([uniqueName]) => uniqueName),
// not requiredInVault:
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophyBronzeRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophyGoldRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophySilverRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/PlagueStarEventTrophyTerracottaRecipe"
// removed in 38.6.0:
// "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyBronzeRecipe",
// "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyCrystalRecipe",
// "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophyGoldRecipe",
// "/Lotus/Levels/ClanDojo/ComponentPropRecipes/ThumperTrophySilverRecipe",
// "/Lotus/Levels/ClanDojo/ComponentPropRecipes/NaturalPlaceables/CoralChunkARecipe"
]) {
let name = getString(getItemName(uniqueName) || uniqueName, lang);
if (uniqueName.startsWith("/Lotus/Levels/ClanDojo/ComponentPropRecipes/GradivusDilemma")) {
const factionTag = uniqueName.includes("Corpus") ? "FC_CORPUS" : "FC_GRINEER";
const faction = ExportFactions[factionTag].name;
name += ` [${getString(faction, lang)}]`;
}
res.VaultDecoRecipes.push({
uniqueName,
name
});
}
for (const [uniqueName, item] of Object.entries(ExportFlavour)) {
res.FlavourItems.push({
uniqueName,
name: getString(item.name, lang),
alwaysAvailable: item.alwaysAvailable
});
}
response.json(res);
};

View File

@ -3,7 +3,6 @@ import { getInventory } from "../../services/inventoryService.ts";
import { getLoadout } from "../../services/loadoutService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import type { IInventoryClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IGetShipResponse } from "../../types/personalRoomsTypes.ts";
import type { RequestHandler } from "express";
@ -33,7 +32,6 @@ export const importController: RequestHandler = async (req, res) => {
}
res.end();
broadcastInventoryUpdate(req);
};
interface IImportRequest {

View File

@ -9,7 +9,6 @@ import {
import { logger } from "../../utils/logger.ts";
import type { RequestHandler } from "express";
import { ExportKeys } from "warframe-public-export-plus";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const manageQuestsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -61,7 +60,6 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
break;
}
inventory.QuestKeys.pull({ ItemType: questItemType });
if (inventory.ActiveQuest == questItemType) inventory.ActiveQuest = "";
break;
}
case "completeKey": {
@ -103,20 +101,12 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
questKey.Completed = false;
questKey.CompletionDate = undefined;
}
const run = questKey.Progress[0]?.c ?? 0;
const stage = questKey.Progress.map(p => p.c).lastIndexOf(run);
if (run > 0) {
questKey.Progress[stage].c = run - 1;
} else {
questKey.Progress.pop();
}
questKey.Progress.pop();
const stage = questKey.Progress.length - 1;
if (stage > 0) {
await giveKeyChainStageTriggered(inventory, {
KeyChain: questKey.ItemType,
ChainStage: stage - 1
ChainStage: stage
});
}
}
@ -132,28 +122,28 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
}
if (!questKey.Progress) break;
const run = questKey.Progress[0]?.c ?? 0;
const currentStage = questKey.Progress.map(p => p.c).lastIndexOf(run);
const currentStage = questKey.Progress.length;
if (currentStage + 1 == questManifest.chainStages?.length) {
logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`);
await completeQuest(inventory, questKey.ItemType, true);
await completeQuest(inventory, questKey.ItemType);
} else {
if (run > 0) {
questKey.Progress[currentStage + 1].c = run;
} else {
questKey.Progress.push({ c: run, i: false, m: false, b: [] });
}
const progress = {
c: 0,
i: false,
m: false,
b: []
};
questKey.Progress.push(progress);
await giveKeyChainStageTriggered(inventory, {
KeyChain: questKey.ItemType,
ChainStage: currentStage + 1
ChainStage: currentStage
});
if (currentStage > 0) {
await giveKeyChainMissionReward(inventory, {
KeyChain: questKey.ItemType,
ChainStage: currentStage
ChainStage: currentStage - 1
});
}
}
@ -167,5 +157,4 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
await inventory.save();
res.status(200).end();
broadcastInventoryUpdate(req);
};

View File

@ -1,7 +1,6 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -13,7 +12,6 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
);
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
return;
}
res.status(400).end();

View File

@ -1,7 +1,6 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -16,7 +15,6 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
}
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
return;
}
}

View File

@ -1,14 +0,0 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const removeCustomizationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const ItemType = req.query.itemType as string;
const inventory = await getInventory(accountId, "FlavourItems");
inventory.FlavourItems.pull({ ItemType });
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};

View File

@ -1,24 +0,0 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { equipmentKeys } from "../../types/inventoryTypes/inventoryTypes.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const removeIsNewController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const filteredEquipmentKeys = equipmentKeys.filter(k => k !== "CrewShipWeapons" && k !== "CrewShipSalvagedWeapons");
const inventory = await getInventory(accountId, [...filteredEquipmentKeys, "WeaponSkins"].join(" "));
for (const key of filteredEquipmentKeys) {
if (key in inventory) {
for (const equipment of inventory[key]) {
if (equipment.IsNew) equipment.IsNew = false;
}
}
}
for (const equipment of inventory.WeaponSkins) {
if (equipment.IsNew) equipment.IsNew = false;
}
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};

View File

@ -1,31 +1,18 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { sendWsBroadcastEx, sendWsBroadcastTo } from "../../services/wsService.ts";
import type { IAccountCheats } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
import { logger } from "../../utils/logger.ts";
export const setAccountCheatController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = req.body as ISetAccountCheatRequest;
const inventory = await getInventory(accountId, payload.key);
if (payload.value == undefined) {
logger.warn(`Aborting setting ${payload.key} as undefined!`);
return;
}
inventory[payload.key] = payload.value as never;
inventory[payload.key] = payload.value;
await inventory.save();
res.end();
if (["infiniteCredits", "infinitePlatinum", "infiniteEndo", "infiniteRegalAya"].indexOf(payload.key) != -1) {
sendWsBroadcastTo(accountId, { update_inventory: true, sync_inventory: true });
} else {
sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
}
};
interface ISetAccountCheatRequest {
key: keyof IAccountCheats;
value: IAccountCheats[keyof IAccountCheats];
value: boolean;
}

View File

@ -1,22 +1,45 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import type { IBooster } from "../../types/inventoryTypes/inventoryTypes.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { ExportBoosters } from "warframe-public-export-plus";
const I32_MAX = 0x7fffffff;
export const setBoosterController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as IBooster[];
const requests = req.body as { ItemType: string; ExpiryDate: number }[];
const inventory = await getInventory(accountId, "Boosters");
for (const request of requests) {
const index = inventory.Boosters.findIndex(item => item.ItemType === request.ItemType);
if (index !== -1) {
inventory.Boosters[index].ExpiryDate = request.ExpiryDate;
const boosters = inventory.Boosters;
if (
requests.some(request => {
if (typeof request.ItemType !== "string") return true;
if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined)
return true;
if (typeof request.ExpiryDate !== "number") return true;
if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true;
return false;
})
) {
res.status(400).send("Invalid ItemType provided.");
return;
}
const now = Math.trunc(Date.now() / 1000);
for (const { ItemType, ExpiryDate } of requests) {
if (ExpiryDate <= now) {
// remove expired boosters
const index = boosters.findIndex(item => item.ItemType === ItemType);
if (index !== -1) {
boosters.splice(index, 1);
}
} else {
inventory.Boosters.push(request);
const boosterItem = boosters.find(item => item.ItemType === ItemType);
if (boosterItem) {
boosterItem.ExpiryDate = ExpiryDate;
} else {
boosters.push({ ItemType, ExpiryDate });
}
}
}
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};

View File

@ -1,7 +1,6 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const setEvolutionProgressController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -26,7 +25,6 @@ export const setEvolutionProgressController: RequestHandler = async (req, res) =
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};
type ISetEvolutionProgressRequest = {

View File

@ -1,29 +0,0 @@
import { GuildMember } from "../../models/guildModel.ts";
import { getGuildForRequestEx } from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IGuildCheats } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
export const setGuildCheatController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = req.body as ISetGuildCheatRequest;
const inventory = await getInventory(accountId, `GuildId`);
const guild = await getGuildForRequestEx(req, inventory);
const member = await GuildMember.findOne({ accountId: accountId, guildId: guild._id });
if (member) {
if (member.rank > 1) {
res.end();
return;
}
guild[payload.key] = payload.value;
await guild.save();
}
res.end();
};
interface ISetGuildCheatRequest {
key: keyof IGuildCheats;
value: boolean;
}

View File

@ -1,27 +0,0 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
export const setInvigorationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as ISetInvigorationRequest;
const inventory = await getInventory(accountId, "Suits");
const suit = inventory.Suits.id(request.oid);
if (suit) {
const hasUpgrades = request.DefensiveUpgrade && request.OffensiveUpgrade && request.UpgradesExpiry;
suit.DefensiveUpgrade = hasUpgrades ? request.DefensiveUpgrade : undefined;
suit.OffensiveUpgrade = hasUpgrades ? request.OffensiveUpgrade : undefined;
suit.UpgradesExpiry = hasUpgrades ? new Date(request.UpgradesExpiry) : undefined;
await inventory.save();
broadcastInventoryUpdate(req);
}
res.end();
};
interface ISetInvigorationRequest {
oid: string;
DefensiveUpgrade: string;
OffensiveUpgrade: string;
UpgradesExpiry: number;
}

View File

@ -1,127 +0,0 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import {
getGuildForRequestEx,
setGuildTechLogState,
processFundedGuildTechProject,
scaleRequiredCount,
hasGuildPermission,
addGuildMemberMiscItemContribution,
processGuildTechProjectContributionsUpdate,
processCompletedGuildTechProject
} from "../../services/guildService.ts";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { GuildPermission } from "../../types/guildTypes.ts";
import { GuildMember } from "../../models/guildModel.ts";
export const addTechProjectController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as ITechProjectRequest[];
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
guild.TechProjects ??= [];
for (const request of requests) {
const recipe = ExportDojoRecipes.research[request.ItemType];
if (!guild.TechProjects.find(x => x.ItemType == request.ItemType)) {
const techProject =
guild.TechProjects[
guild.TechProjects.push({
ItemType: request.ItemType,
ReqCredits: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: guild.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})),
State: 0
}) - 1
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (guild.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
}
}
}
await guild.save();
res.end();
};
export const removeTechProjectController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as ITechProjectRequest[];
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
guild.TechProjects ??= [];
for (const request of requests) {
const index = guild.TechProjects.findIndex(x => x.ItemType === request.ItemType);
if (index !== -1) {
guild.TechProjects.splice(index, 1);
}
}
await guild.save();
res.end();
};
export const fundTechProjectController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as ITechProjectRequest[];
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
for (const request of requests) {
const techProject = guild.TechProjects!.find(x => x.ItemType == request.ItemType)!;
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += techProject.ReqCredits;
techProject.ReqCredits = 0;
for (const reqItem of techProject.ReqItems) {
addGuildMemberMiscItemContribution(guildMember, reqItem);
reqItem.ItemCount = 0;
}
await processGuildTechProjectContributionsUpdate(guild, techProject);
}
await Promise.all([guild.save(), guildMember.save()]);
res.end();
};
export const completeTechProjectsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as ITechProjectRequest[];
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
for (const request of requests) {
const techProject = guild.TechProjects!.find(x => x.ItemType == request.ItemType)!;
techProject.CompletionDate = new Date();
if (setGuildTechLogState(guild, techProject.ItemType, 4, techProject.CompletionDate)) {
processCompletedGuildTechProject(guild, techProject.ItemType);
}
}
await guild.save();
res.end();
};
interface ITechProjectRequest {
ItemType: string;
}

View File

@ -1,9 +1,16 @@
import type { RequestHandler } from "express";
import type { ITunables } from "../../types/bootstrapperTypes.ts";
// This endpoint is specific to the OpenWF Bootstrapper: https://openwf.io/bootstrapper-manual
export const tunablesController: RequestHandler = (_req, res) => {
interface ITunables {
prohibit_skip_mission_start_timer?: boolean;
prohibit_fov_override?: boolean;
prohibit_freecam?: boolean;
prohibit_teleport?: boolean;
prohibit_scripts?: boolean;
}
const tunablesController: RequestHandler = (_req, res) => {
const tunables: ITunables = {};
//tunables.prohibit_skip_mission_start_timer = true;
//tunables.prohibit_fov_override = true;
@ -12,3 +19,5 @@ export const tunablesController: RequestHandler = (_req, res) => {
//tunables.prohibit_scripts = true;
res.json(tunables);
};
export { tunablesController };

View File

@ -1,42 +0,0 @@
import type { RequestHandler } from "express";
import { ExportResources, ExportVirtuals } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addItem, getInventory } from "../../services/inventoryService.ts";
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
export const unlockAllCapturaScenesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
let needSync = false;
for (const uniqueName of Object.keys(ExportResources)) {
if (resourceInheritsFrom(uniqueName, "/Lotus/Types/Items/MiscItems/PhotoboothTile")) {
await addItem(inventory, uniqueName, 1);
needSync = true;
}
}
await inventory.save();
res.end();
if (needSync) {
sendWsBroadcastToGame(accountId, { sync_inventory: true });
}
};
const resourceInheritsFrom = (resourceName: string, targetName: string): boolean => {
let parentName = resourceGetParent(resourceName);
for (; parentName != undefined; parentName = resourceGetParent(parentName)) {
if (parentName == targetName) {
return true;
}
}
return false;
};
const resourceGetParent = (resourceName: string): string | undefined => {
if (resourceName in ExportResources) {
return ExportResources[resourceName].parentName;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName;
};

View File

@ -1,7 +1,6 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
export const unlockAllIntrinsicsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -17,5 +16,4 @@ export const unlockAllIntrinsicsController: RequestHandler = async (req, res) =>
inventory.PlayerSkills.LPS_DRIFT_ENDURANCE = 10;
await inventory.save();
res.end();
sendWsBroadcastToGame(accountId, { sync_inventory: true });
};

View File

@ -1,7 +1,6 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
const allEudicoHeistJobs = [
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne",
@ -22,5 +21,4 @@ export const unlockAllProfitTakerStagesController: RequestHandler = async (req,
}
await inventory.save();
res.end();
sendWsBroadcastToGame(accountId, { sync_inventory: true });
};

View File

@ -1,23 +0,0 @@
import type { RequestHandler } from "express";
import allScans from "../../../static/fixed_responses/allScans.json" with { type: "json" };
import { ExportEnemies } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getStats } from "../../services/statsService.ts";
export const unlockAllScansController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const stats = await getStats(accountId);
const scanTypes = new Set<string>(allScans);
for (const type of Object.keys(ExportEnemies.avatars)) {
scanTypes.add(type);
}
stats.Scans = [];
for (const type of scanTypes) {
stats.Scans.push({ type, scans: 9999 });
}
await stats.save();
res.end();
};

View File

@ -1,19 +0,0 @@
import type { RequestHandler } from "express";
import allShipFeatures from "../../../static/fixed_responses/allShipFeatures.json" with { type: "json" };
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
export const unlockAllShipFeaturesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId);
const featureSet = new Set(personalRooms.Ship.Features);
for (const feature of allShipFeatures) {
if (!featureSet.has(feature)) {
personalRooms.Ship.Features.push(feature);
}
}
await personalRooms.save();
res.end();
};

View File

@ -1,7 +1,6 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
export const unlockAllSimarisResearchEntriesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -18,5 +17,4 @@ export const unlockAllSimarisResearchEntriesController: RequestHandler = async (
].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
await inventory.save();
res.end();
sendWsBroadcastToGame(accountId, { sync_inventory: true });
};

View File

@ -1,25 +0,0 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
export const unlockLevelCapController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = req.body as IunlockLevelCapRequest;
const inventory = await getInventory(accountId, data.Category);
const equipment = inventory[data.Category].id(data.ItemId)!;
equipment.Polarized ??= 0;
equipment.Polarized = data.Polarized;
await inventory.save();
res.end();
broadcastInventoryUpdate(req);
};
interface IunlockLevelCapRequest {
Category: TEquipmentKey;
ItemId: string;
Polarized: number;
}

View File

@ -2,7 +2,6 @@ import { getInventory } from "../../services/inventoryService.ts";
import type { WeaponTypeInternal } from "../../services/itemDataService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { sendWsBroadcastToGame } from "../../services/wsService.ts";
export const updateFingerprintController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -23,7 +22,6 @@ export const updateFingerprintController: RequestHandler = async (req, res) => {
await inventory.save();
}
res.end();
sendWsBroadcastToGame(accountId, { sync_inventory: true });
};
interface IUpdateFingerPrintRequest {

View File

@ -20,14 +20,13 @@ import type {
} from "../../types/inventoryTypes/inventoryTypes.ts";
import { LoadoutIndex } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { catBreadHash, getJSONfromString } from "../../helpers/stringHelpers.ts";
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
import type { IStatsClient } from "../../types/statTypes.ts";
import { toStoreItem } from "../../services/itemDataService.ts";
import type { FlattenMaps } from "mongoose";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import type { ILoadoutConfigClient } from "../../types/saveLoadoutTypes.ts";
import { skinLookupTable } from "../../helpers/skinLookupTable.ts";
const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IProfileViewingData | undefined> => {
const account = await Account.findById(playerId, "DisplayName");
@ -262,6 +261,8 @@ interface IXPComponentClient {
locTags?: Record<string, string>;
}
let skinLookupTable: Record<number, string> | undefined;
const resolveAndCollectSkins = (
inventory: TInventoryDatabaseDocument,
skins: Set<string>,
@ -273,6 +274,12 @@ const resolveAndCollectSkins = (
// Resolve oids to type names
if (config.Skins[i].length == 24) {
if (config.Skins[i].substring(0, 16) == "ca70ca70ca70ca70") {
if (!skinLookupTable) {
skinLookupTable = {};
for (const key of Object.keys(ExportCustoms)) {
skinLookupTable[catBreadHash(key)] = key;
}
}
config.Skins[i] = skinLookupTable[parseInt(config.Skins[i].substring(16), 16)];
} else {
const skinItem = inventory.WeaponSkins.id(config.Skins[i]);

View File

@ -1,12 +1,5 @@
import type { RequestHandler } from "express";
import { getAccountForRequest } from "../../services/loginService.ts";
import { version_compare } from "../../helpers/inventoryHelpers.ts";
export const getSkuCatalogController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (!account.BuildLabel || version_compare(account.BuildLabel, "2025.10.14.16.10") >= 0) {
res.sendFile("static/fixed_responses/getSkuCatalogU40.json", { root: "./" });
} else {
res.sendFile("static/fixed_responses/getSkuCatalog.json", { root: "./" });
}
export const getSkuCatalogController: RequestHandler = (_req, res) => {
res.sendFile("static/fixed_responses/getSkuCatalog.json", { root: "./" });
};

View File

@ -1,5 +1,8 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { config } from "../../services/configService.ts";
import allScans from "../../../static/fixed_responses/allScans.json" with { type: "json" };
import { ExportEnemies } from "warframe-public-export-plus";
import { getInventory } from "../../services/inventoryService.ts";
import { getStats } from "../../services/statsService.ts";
import type { IStatsClient } from "../../types/statTypes.ts";
@ -9,7 +12,7 @@ const viewController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId, "XPInfo");
const playerStats = await getStats(accountId);
const responseJson = playerStats.toJSON<IStatsClient>();
const responseJson = playerStats.toJSON() as IStatsClient;
responseJson.Weapons ??= [];
for (const item of inventory.XPInfo) {
const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType);
@ -19,6 +22,24 @@ const viewController: RequestHandler = async (req, res) => {
responseJson.Weapons.push({ type: item.ItemType, xp: item.XP });
}
}
if (config.unlockAllScans) {
const scans = new Set(allScans);
for (const type of Object.keys(ExportEnemies.avatars)) {
if (!scans.has(type)) scans.add(type);
}
// Take any existing scans and also set them to 9999
if (responseJson.Scans) {
for (const scan of responseJson.Scans) {
scans.add(scan.type);
}
}
responseJson.Scans = [];
for (const type of scans) {
responseJson.Scans.push({ type: type, scans: 9999 });
}
}
res.json(responseJson);
};

View File

@ -212,12 +212,12 @@ export const getInfNodes = (manifest: INemesisManifest, rank: number): IInfNode[
value.systemIndex === systemIndex &&
value.nodeType != 3 && // not hub
value.nodeType != 7 && // not junction
value.missionType != "MT_ASSASSINATION" &&
value.missionType != "MT_LANDSCAPE" &&
value.missionType != "MT_RAILJACK" &&
value.missionType != "MT_OFFERING" &&
value.missionType != "MT_PVPVE" &&
value.name.indexOf("1999NodeI") == -1 && // not stage defense
value.missionIndex && // must have a mission type and not assassination
value.missionIndex != 28 && // not open world
value.missionIndex != 32 && // not railjack
value.missionIndex != 41 && // not saya's visions
value.missionIndex != 42 && // not face off
value.name.indexOf("1999NodeI") == -1 && // not stage defence
value.name.indexOf("1999NodeJ") == -1 && // not lich bounty
!isArchwingMission(value)
) {

View File

@ -0,0 +1,23 @@
import type { SlotPurchase, SlotPurchaseName } from "../types/purchaseTypes.ts";
export const slotPurchaseNameToSlotName: SlotPurchase = {
SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 },
TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 },
TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 },
SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 },
TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 },
MechSlotItem: { name: "MechBin", purchaseQuantity: 1 },
TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 },
RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 },
TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 },
CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 }
};
export const isSlotPurchaseName = (slotPurchaseName: string): slotPurchaseName is SlotPurchaseName => {
return slotPurchaseName in slotPurchaseNameToSlotName;
};
export const parseSlotPurchaseName = (slotPurchaseName: string): SlotPurchaseName => {
if (!isSlotPurchaseName(slotPurchaseName)) throw new Error(`invalid slot name ${slotPurchaseName}`);
return slotPurchaseName;
};

View File

@ -8,6 +8,7 @@ import { logger } from "../utils/logger.ts";
import { addMiscItems, combineInventoryChanges } from "../services/inventoryService.ts";
import { handleStoreItemAcquisition } from "../services/purchaseService.ts";
import type { IInventoryChanges } from "../types/purchaseTypes.ts";
import { config } from "../services/configService.ts";
export const crackRelic = async (
inventory: TInventoryDatabaseDocument,
@ -28,10 +29,10 @@ export const crackRelic = async (
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights
)!;
if (inventory.relicRewardItemCountMultiplier && inventory.relicRewardItemCountMultiplier != 1) {
if (config.relicRewardItemCountMultiplier !== undefined && (config.relicRewardItemCountMultiplier ?? 1) != 1) {
reward = {
...reward,
itemCount: reward.itemCount * inventory.relicRewardItemCountMultiplier
itemCount: reward.itemCount * config.relicRewardItemCountMultiplier
};
}
logger.debug(`relic rolled`, reward);
@ -53,9 +54,6 @@ export const crackRelic = async (
(await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges
);
// Client has picked its own reward (for lack of choice)
participant.ChosenRewardOwner = participant.AccountId;
return reward;
};

View File

@ -1,8 +0,0 @@
import { ExportCustoms } from "warframe-public-export-plus";
import { catBreadHash } from "./stringHelpers.ts";
export const skinLookupTable: Record<number, string> = {};
for (const key of Object.keys(ExportCustoms)) {
skinLookupTable[catBreadHash(key)] = key;
}

Some files were not shown because too many files have changed in this diff Show More