Compare commits

..

2 Commits

Author SHA1 Message Date
ce9d9f1a17 prettier
All checks were successful
Build / build (push) Successful in 45s
Build / build (pull_request) Successful in 1m33s
2025-04-22 21:59:02 +02:00
193d5b155e chore: provide tileset for sortie missions
Some checks failed
Build / build (push) Failing after 44s
Build / build (pull_request) Failing after 1m24s
2025-04-22 21:53:39 +02:00
209 changed files with 12431 additions and 11005 deletions

View File

@ -1,7 +1,6 @@
name: Build name: Build
on: on:
push: push: {}
branches: ["main"]
pull_request: {} pull_request: {}
jobs: jobs:
build: build:
@ -11,8 +10,6 @@ jobs:
uses: actions/checkout@v4.1.2 uses: actions/checkout@v4.1.2
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with:
node-version: ">=20.6.0"
- run: npm ci - run: npm ci
- run: cp config.json.example config.json - run: cp config.json.example config.json
- run: npm run verify - run: npm run verify

View File

@ -4,9 +4,9 @@ on:
branches: branches:
- main - main
jobs: jobs:
docker-amd64: docker:
if: github.repository == 'OpenWF/SpaceNinjaServer' if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: amd64 runs-on: ubuntu-latest
steps: steps:
- name: Set up Docker buildx - name: Set up Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -18,27 +18,8 @@ jobs:
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
openwf/spaceninjaserver:latest openwf/spaceninjaserver:latest
openwf/spaceninjaserver:${{ github.sha }} 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

View File

@ -1,17 +0,0 @@
## 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,11 +1,26 @@
FROM node:24-alpine3.21 FROM node:18-alpine3.19
RUN apk add --no-cache bash jq ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF
ENV APP_MY_ADDRESS=localhost
ENV APP_HTTP_PORT=80
ENV APP_HTTPS_PORT=443
ENV APP_AUTO_CREATE_ACCOUNT=true
ENV APP_SKIP_STORY_MODE_CHOICE=true
ENV APP_SKIP_TUTORIAL=true
ENV APP_SKIP_ALL_DIALOGUE=true
ENV APP_UNLOCK_ALL_SCANS=true
ENV APP_UNLOCK_ALL_MISSIONS=true
ENV APP_INFINITE_RESOURCES=true
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true
ENV APP_UNLOCK_ALL_SKINS=true
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true
ENV APP_SPOOF_MASTERY_RANK=-1
RUN apk add --no-cache bash sed wget jq
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN npm i --omit=dev
RUN npm run build
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -14,24 +14,4 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `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.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
- `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:
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
- `RadioLegionIntermission4Syndicate` for Nora's Choice
- `RadioLegionIntermission3Syndicate` for Intermission III
- `RadioLegion3Syndicate` for Glassmaker
- `RadioLegionIntermission2Syndicate` for Intermission II
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six
- `worldState.circuitGameModes` can be provided with an array of valid game modes (`Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, `Alchemy`)

View File

@ -1,9 +1,10 @@
@echo off @echo off
echo Updating SpaceNinjaServer... echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune git fetch --prune
git stash git stash
git checkout -f origin/main git reset --hard origin/main
if exist static\data\0\ ( if exist static\data\0\ (
echo Updating stripped assets... echo Updating stripped assets...

View File

@ -1,23 +0,0 @@
#!/bin/bash
echo "Updating SpaceNinjaServer..."
git fetch --prune
git stash
git checkout -f origin/main
if [ -d "static/data/0/" ]; then
echo "Updating stripped assets..."
cd static/data/0/
git pull
cd ../../../
fi
echo "Updating dependencies..."
npm i --omit=dev
npm run build
if [ $? -eq 0 ]; then
npm run start
echo "SpaceNinjaServer seems to have crashed."
fi

View File

@ -13,18 +13,12 @@
"skipTutorial": false, "skipTutorial": false,
"skipAllDialogue": false, "skipAllDialogue": false,
"unlockAllScans": false, "unlockAllScans": false,
"unlockAllMissions": false,
"infiniteCredits": false, "infiniteCredits": false,
"infinitePlatinum": false, "infinitePlatinum": false,
"infiniteEndo": false, "infiniteEndo": false,
"infiniteRegalAya": false, "infiniteRegalAya": false,
"infiniteHelminthMaterials": false, "infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false,
"dontSubtractPurchaseCreditCost": false,
"dontSubtractPurchasePlatinumCost": false,
"dontSubtractPurchaseItemCost": false,
"dontSubtractPurchaseStandingCost": false,
"dontSubtractVoidTraces": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false, "unlockAllShipFeatures": false,
"unlockAllShipDecorations": false, "unlockAllShipDecorations": false,
"unlockAllFlavourItems": false, "unlockAllFlavourItems": false,
@ -38,45 +32,23 @@
"noDailyFocusLimit": false, "noDailyFocusLimit": false,
"noArgonCrystalDecay": false, "noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false, "noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": false, "noVendorPurchaseLimits": true,
"noDeathMarks": false, "noDeathMarks": false,
"noKimCooldowns": false, "noKimCooldowns": false,
"fullyStockedVendors": false,
"baroAlwaysAvailable": false,
"baroFullyStocked": false,
"syndicateMissionsRepeatable": false,
"unlockAllProfitTakerStages": false,
"instantFinishRivenChallenge": false,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false, "noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false, "noDojoRoomBuildStage": false,
"noDecoBuildStage": false, "noDecoBuildStage": false,
"fastDojoRoomDestruction": false, "fastDojoRoomDestruction": false,
"noDojoResearchCosts": false, "noDojoResearchCosts": false,
"noDojoResearchTime": false, "noDojoResearchTime": false,
"fastClanAscension": false, "fastClanAscension": false,
"missionsCanGiveAllRelics": false,
"unlockAllSimarisResearchEntries": false,
"spoofMasteryRank": -1, "spoofMasteryRank": -1,
"nightwaveStandingMultiplier": 1,
"unfaithfulBugFixes": {
"ignore1999LastRegionPlayed": false,
"fixXtraCheeseTimer": false
},
"worldState": { "worldState": {
"creditBoost": false, "creditBoost": false,
"affinityBoost": false, "affinityBoost": false,
"resourceBoost": false, "resourceBoost": false,
"starDays": true, "starDays": true,
"galleonOfGhouls": 0, "lockTime": 0
"eidolonOverride": "",
"vallisOverride": "",
"duviriOverride": "",
"nightwaveOverride": "",
"circuitGameModes": null
},
"dev": {
"keepVendorsExpired": false
} }
} }

View File

@ -1,20 +1,37 @@
services: services:
spaceninjaserver: spaceninjaserver:
# The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'. # build: .
image: openwf/spaceninjaserver:latest image: openwf/spaceninjaserver:latest
environment:
APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
# Following environment variables are set to default image values.
# Uncomment to edit.
# APP_MY_ADDRESS: localhost
# APP_HTTP_PORT: 80
# APP_HTTPS_PORT: 443
# APP_AUTO_CREATE_ACCOUNT: true
# APP_SKIP_STORY_MODE_CHOICE: true
# APP_SKIP_TUTORIAL: true
# APP_SKIP_ALL_DIALOGUE: true
# APP_UNLOCK_ALL_SCANS: true
# APP_UNLOCK_ALL_MISSIONS: true
# APP_UNLOCK_ALL_QUESTS: true
# APP_COMPLETE_ALL_QUESTS: true
# APP_INFINITE_RESOURCES: true
# APP_UNLOCK_ALL_SHIP_FEATURES: true
# APP_UNLOCK_ALL_SHIP_DECORATIONS: true
# APP_UNLOCK_ALL_FLAVOUR_ITEMS: true
# APP_UNLOCK_ALL_SKINS: true
# APP_UNIVERSAL_POLARITY_EVERYWHERE: true
# APP_SPOOF_MASTERY_RANK: -1
volumes: volumes:
- ./docker-data/conf:/app/conf - ./docker-data/static:/app/static/data
- ./docker-data/static-data:/app/static/data
- ./docker-data/logs:/app/logs - ./docker-data/logs:/app/logs
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
# Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
#build: .
# Works best when using `docker-compose up --force-recreate --build`.
depends_on: depends_on:
- mongodb - mongodb
mongodb: mongodb:
@ -24,4 +41,3 @@ services:
MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
volumes: volumes:
- ./docker-data/database:/data/db - ./docker-data/database:/data/db
command: mongod --quiet --logpath /dev/null

View File

@ -1,8 +1,23 @@
#!/bin/bash #!/bin/bash
set -e set -e
if [ ! -f conf/config.json ]; then # Set up the configuration file using environment variables.
jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config.json.example > /app/conf/config.json echo '{
fi "logger": {
"files": true,
"level": "trace",
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
}
}
' > config.json
exec npm run start -- --configPath conf/config.json for config in $(env | grep "APP_")
do
var=$(echo "${config}" | tr '[:upper:]' '[:lower:]' | sed 's/app_//g' | sed -E 's/_([a-z])/\U\1/g' | sed 's/=.*//g')
val=$(echo "${config}" | sed 's/.*=//g')
jq --arg variable "$var" --arg value "$val" '.[$variable] += try [$value|fromjson][] catch $value' config.json > config.tmp
mv config.tmp config.json
done
npm install
exec npm run dev

1308
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,16 +5,9 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
"build": "tsgo --sourceMap && ncp static/webui build/static/webui", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "build": "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.js",
"dev:bun": "bun scripts/dev.js",
"verify": "tsgo --noEmit", "verify": "tsgo --noEmit",
"bun-run": "bun src/index.ts",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .", "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
@ -25,30 +18,30 @@
"dependencies": { "dependencies": {
"@types/express": "^5", "@types/express": "^5",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "^7.0.0-dev.20250625.1",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.4.4", "json-with-bigint": "^3.2.2",
"mongoose": "^8.11.0", "mongoose": "^8.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"undici": "^7.10.0", "warframe-public-export-plus": "^0.5.56",
"warframe-public-export-plus": "^0.5.71",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0"
"ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@rxliuli/tsgo": "^2025.3.31",
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.28.0",
"chokidar": "^4.0.3",
"eslint": "^8", "eslint": "^8",
"eslint-plugin-prettier": "^5.2.5", "eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"tree-kill": "^1.2.2" "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0"
},
"engines": {
"node": ">=18.15.0",
"npm": ">=9.5.0"
} }
} }

View File

@ -1,58 +0,0 @@
/* eslint-disable */
const { spawn } = require("child_process");
const chokidar = require("chokidar");
const kill = require("tree-kill");
let secret = "";
for (let i = 0; i != 10; ++i) {
secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41);
}
const args = [...process.argv].splice(2);
args.push("--dev");
args.push("--secret");
args.push(secret);
let buildproc, runproc;
const spawnopts = { stdio: "inherit", shell: true };
function run(changedFile) {
if (changedFile) {
console.log(`Change to ${changedFile} detected`);
}
if (buildproc) {
kill(buildproc.pid);
buildproc = undefined;
}
if (runproc) {
kill(runproc.pid);
runproc = undefined;
}
const thisbuildproc = spawn("npm", ["run", process.versions.bun ? "verify" : "build:dev"], spawnopts);
const thisbuildstart = Date.now();
buildproc = thisbuildproc;
buildproc.on("exit", code => {
if (buildproc !== thisbuildproc) {
return;
}
buildproc = undefined;
if (code === 0) {
console.log(`${process.versions.bun ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
runproc = spawn("npm", ["run", process.versions.bun ? "bun-run" : "start", "--", ...args], spawnopts);
runproc.on("exit", () => {
runproc = undefined;
});
}
});
}
run();
chokidar.watch("src").on("change", run);
chokidar.watch("static/fixed_responses").on("change", run);
chokidar.watch("static/webui").on("change", async () => {
try {
await fetch("http://localhost/custom/webuiFileChangeDetected?secret=" + secret);
} catch (e) {}
});

View File

@ -1,7 +1,6 @@
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php // Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
// Converted via ChatGPT-4o // Converted via ChatGPT-4o
/* eslint-disable */
const fs = require("fs"); const fs = require("fs");
function extractStrings(content) { function extractStrings(content) {

View File

@ -21,13 +21,6 @@ app.use((req, _res, next) => {
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined; req.headers["content-encoding"] = undefined;
} }
// U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
// U17 sets no Content-Type at all, which Express also doesn't like.
if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
req.headers["content-type"] = "application/octet-stream";
}
next(); next();
}); });

View File

@ -2,18 +2,15 @@ const millisecondsPerSecond = 1000;
const secondsPerMinute = 60; const secondsPerMinute = 60;
const minutesPerHour = 60; const minutesPerHour = 60;
const hoursPerDay = 24; const hoursPerDay = 24;
const daysPerWeek = 7;
const unixSecond = millisecondsPerSecond; const unixSecond = millisecondsPerSecond;
const unixMinute = secondsPerMinute * millisecondsPerSecond; const unixMinute = secondsPerMinute * millisecondsPerSecond;
const unixHour = unixMinute * minutesPerHour; const unixHour = unixMinute * minutesPerHour;
const unixDay = hoursPerDay * unixHour; const unixDay = hoursPerDay * unixHour;
const unixWeek = daysPerWeek * unixDay;
export const unixTimesInMs = { export const unixTimesInMs = {
second: unixSecond, second: unixSecond,
minute: unixMinute, minute: unixMinute,
hour: unixHour, hour: unixHour,
day: unixDay, day: unixDay
week: unixWeek
}; };

View File

@ -1,16 +1,11 @@
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
createVeiledRivenFingerprint,
createUnveiledRivenFingerprint,
rivenRawToRealWeighted
} from "@/src/helpers/rivenHelper";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMods, getInventory } from "@/src/services/inventoryService"; import { addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement } from "@/src/services/rngService"; import { getRandomElement } from "@/src/services/rngService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportUpgrades } from "warframe-public-export-plus"; import { ExportUpgrades } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const activateRandomModController: RequestHandler = async (req, res) => { export const activateRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -22,10 +17,8 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
ItemCount: -1 ItemCount: -1
} }
]); ]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!; const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]);
const fingerprint = config.instantFinishRivenChallenge const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
? createUnveiledRivenFingerprint(ExportUpgrades[rivenType])
: createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex = const upgradeIndex =
inventory.Upgrades.push({ inventory.Upgrades.push({
ItemType: rivenType, ItemType: rivenType,

View File

@ -1,60 +0,0 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes";
import { RequestHandler } from "express";
export const addFriendController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<IAddFriendRequest>(String(req.body));
const promises: Promise<void>[] = [];
const newFriends: IFriendInfo[] = [];
if (payload.friend == "all") {
const [internalFriendships, externalFriendships] = await Promise.all([
Friendship.find({ owner: accountId }, "friend"),
Friendship.find({ friend: accountId }, "owner")
]);
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
promises.push(
Friendship.insertOne({
owner: accountId,
friend: externalFriendship.owner,
Note: externalFriendship.Note // TOVERIFY: Should the note be copied when accepting a friend request?
}) as unknown as Promise<void>
);
newFriends.push({
_id: toOid(externalFriendship.owner)
});
}
}
} else {
const externalFriendship = await Friendship.findOne({ owner: payload.friend, friend: accountId }, "Note");
if (externalFriendship) {
promises.push(
Friendship.insertOne({
owner: accountId,
friend: payload.friend,
Note: externalFriendship.Note
}) as unknown as Promise<void>
);
newFriends.push({
_id: { $oid: payload.friend }
});
}
}
for (const newFriend of newFriends) {
promises.push(addAccountDataToFriendInfo(newFriend));
promises.push(addInventoryDataToFriendInfo(newFriend));
}
await Promise.all(promises);
res.json({
Friends: newFriends
});
};
interface IAddFriendRequest {
friend: string; // oid or "all" in which case all=1 is also a query parameter
}

View File

@ -2,7 +2,7 @@ import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel"; import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes"; import { IFriendInfo } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const addIgnoredUserController: RequestHandler = async (req, res) => { export const addIgnoredUserController: RequestHandler = async (req, res) => {

View File

@ -1,52 +0,0 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "@/src/services/friendService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes";
import { RequestHandler } from "express";
export const addPendingFriendController: RequestHandler = async (req, res) => {
const payload = getJSONfromString<IAddPendingFriendRequest>(String(req.body));
const account = await Account.findOne({ DisplayName: payload.friend });
if (!account) {
res.status(400).end();
return;
}
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "Settings");
if (
inventory.Settings?.FriendInvRestriction == "GIFT_MODE_NONE" ||
(inventory.Settings?.FriendInvRestriction == "GIFT_MODE_FRIENDS" &&
!(await areFriendsOfFriends(account._id, accountId)))
) {
res.status(400).send("Friend Invite Restriction");
return;
}
await Friendship.insertOne({
owner: accountId,
friend: account._id,
Note: payload.message
});
const friendInfo: IFriendInfo = {
_id: toOid(account._id),
DisplayName: account.DisplayName,
LastLogin: toMongoDate(account.LastLogin),
Note: payload.message
};
await addInventoryDataToFriendInfo(friendInfo);
res.json({
Friend: friendInfo
});
};
interface IAddPendingFriendRequest {
friend: string;
message: string;
}

View File

@ -1,7 +1,7 @@
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers"; import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
@ -75,7 +75,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!; const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType"); const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!; const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
const alliance = (await Alliance.findById(req.query.allianceId as string, "Name"))!; const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!;
await createMessage(invitedClanOwnerMember.accountId, [ await createMessage(invitedClanOwnerMember.accountId, [
{ {
sndr: getSuffixedName(account), sndr: getSuffixedName(account),
@ -95,7 +95,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title", sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: alliance._id.toString(), contextInfo: alliance._id.toString(),
highPriority: true, highPriority: true,
acceptAction: "ALLIANCE_INVITE", acceptAction: "ALLIANCE_INVITE",

View File

@ -1,10 +1,8 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService"; import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
import { hasGuildPermission } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
@ -24,18 +22,15 @@ export const addToGuildController: RequestHandler = async (req, res) => {
return; return;
} }
const senderAccount = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "Settings"); const inventory = await getInventory(account._id.toString(), "Settings");
if ( // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" || if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") {
(inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" &&
!(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).json("Invite restricted"); res.status(400).json("Invite restricted");
return; return;
} }
const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
const senderAccount = await getAccountForRequest(req);
if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission"); res.status(400).json("Invalid permission");
} }
@ -64,7 +59,7 @@ export const addToGuildController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: payload.GuildId.$oid, contextInfo: payload.GuildId.$oid,
highPriority: true, highPriority: true,
acceptAction: "GUILD_INVITE", acceptAction: "GUILD_INVITE",
@ -76,11 +71,10 @@ export const addToGuildController: RequestHandler = async (req, res) => {
const member: IGuildMemberClient = { const member: IGuildMemberClient = {
_id: { $oid: account._id.toString() }, _id: { $oid: account._id.toString() },
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
LastLogin: toMongoDate(account.LastLogin),
Rank: 7, Rank: 7,
Status: 2 Status: 2
}; };
await addInventoryDataToFriendInfo(member); await fillInInventoryDataForGuildMember(member);
res.json({ NewMember: member }); res.json({ NewMember: member });
} else if ("RequestMsg" in payload) { } else if ("RequestMsg" in payload) {
// Player applying to join a clan // Player applying to join a clan

View File

@ -1,27 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const adoptPetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets");
const data = getJSONfromString<IAdoptPetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.Name = data.name;
await inventory.save();
res.json({
petId: data.petId,
newName: data.name
} satisfies IAdoptPetResponse);
};
interface IAdoptPetRequest {
petId: string;
name: string;
}
interface IAdoptPetResponse {
petId: string;
newName: string;
}

View File

@ -1,9 +1,9 @@
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IOid } from "@/src/types/commonTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus"; import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
@ -24,11 +24,11 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
]); ]);
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
}); });
const rawRivenType = getRandomRawRivenType(); const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!; const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex = const upgradeIndex =
@ -57,8 +57,8 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType]; const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount; counts[meta.rarity] += upgrade.ItemCount;
if (fromOid(upgrade.ItemId) != "000000000000000000000000") { if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else { } else {
addMods(inventory, [ addMods(inventory, [
{ {
@ -128,14 +128,24 @@ const getRandomRawRivenType = (): string => {
}; };
interface IArtifactTransmutationRequest { interface IArtifactTransmutationRequest {
Upgrade: IUpgradeFromClient; Upgrade: IAgnosticUpgradeClient;
LevelDiff: number; LevelDiff: number;
Consumed: IUpgradeFromClient[]; Consumed: IAgnosticUpgradeClient[];
Cost: number; Cost: number;
FusionPointCost: number; FusionPointCost: number;
RivenTransmute?: boolean; RivenTransmute?: boolean;
} }
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [ const specialModSets: string[][] = [
[ [
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",

View File

@ -4,9 +4,9 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
getInventory, getInventory,
updateCurrency, updateCurrency,
@ -17,11 +17,8 @@ import {
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
@ -29,8 +26,10 @@ interface IClaimCompletedRecipeRequest {
export const claimCompletedRecipeController: RequestHandler = async (req, res) => { export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body)); const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString()); if (!accountId) throw new Error("no account id");
const inventory = await getInventory(accountId);
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid); const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) { if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
@ -49,14 +48,40 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
if (req.query.cancel) { if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); ...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
await inventory.save(); await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOidWithLegacySupport[]; let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= []; inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= []; inventory.SpectreLoadouts ??= [];
@ -81,7 +106,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1 1
); );
BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)]; BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
} }
let InventoryChanges: IInventoryChanges = {}; let InventoryChanges: IInventoryChanges = {};
@ -105,73 +130,13 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
...updateCurrency(inventory, cost, true) ...updateCurrency(inventory, cost, true)
}; };
} }
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
if (pet.Details!.HatchDate!.getTime() > Date.now()) {
pet.Details!.HatchDate = new Date();
}
let canSetActive = true;
for (const pet of inventory.KubrowPets) {
if (pet.Details!.Status == Status.StatusAvailable) {
canSetActive = false;
break;
}
}
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await addItem( ...(await addItem(inventory, recipe.resultType, recipe.num, false))
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
}; };
} }
if (
config.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
}
await inventory.save(); await inventory.save();
res.json({ InventoryChanges, BrandedSuits }); res.json({ InventoryChanges, BrandedSuits });
} }
}; };
const refundRecipeIngredients = async (
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase
): Promise<void> => {
updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
};

View File

@ -1,35 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { RequestHandler } from "express";
import { ExportChallenges } from "warframe-public-export-plus";
export const claimJunctionChallengeRewardController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const data = getJSONfromString<IClaimJunctionChallengeRewardRequest>(String(req.body));
const challengeProgress = inventory.ChallengeProgress.find(x => x.Name == data.Challenge)!;
if (challengeProgress.ReceivedJunctionReward) {
throw new Error(`attempt to double-claim junction reward`);
}
challengeProgress.ReceivedJunctionReward = true;
inventory.ClaimedJunctionChallengeRewards ??= [];
inventory.ClaimedJunctionChallengeRewards.push(data.Challenge);
const challengeMeta = Object.entries(ExportChallenges).find(arr => arr[0].endsWith("/" + data.Challenge))![1];
const inventoryChanges = {};
for (const reward of challengeMeta.countedRewards!) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount)).InventoryChanges
);
}
await inventory.save();
res.json({
inventoryChanges: inventoryChanges // Yeah, it's "inventoryChanges" in the response here.
});
};
interface IClaimJunctionChallengeRewardRequest {
Challenge: string;
}

View File

@ -1,4 +1,4 @@
import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -17,7 +17,7 @@ export const claimLibraryDailyTaskRewardController: RequestHandler = async (req,
} }
syndicate.Standing += rewardStanding; syndicate.Standing += rewardStanding;
addFusionPoints(inventory, 80 * rewardQuantity); inventory.FusionPoints += 80 * rewardQuantity;
await inventory.save(); await inventory.save();
res.json({ res.json({

View File

@ -1,37 +0,0 @@
import { checkCalendarChallengeCompletion, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getWorldState } from "@/src/services/worldStateService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER
export const completeCalendarEventController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const calendarProgress = getCalendarProgress(inventory);
const currentSeason = getWorldState().KnownCalendarSeasons[0];
let inventoryChanges: IInventoryChanges = {};
const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
const day = currentSeason.Days[dayIndex];
if (day.events.length != 0) {
if (day.events[0].type == "CET_CHALLENGE") {
throw new Error(`completeCalendarEvent should not be used for challenges`);
}
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
if (selection.type == "CET_REWARD") {
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
} else if (selection.type == "CET_UPGRADE") {
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
} else if (selection.type != "CET_PLOT") {
throw new Error(`unexpected selection type: ${selection.type}`);
}
}
calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
CalendarProgress: inventory.CalendarProgress
});
};

View File

@ -1,14 +1,8 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
deleteGuild, import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
getGuildClient,
giveClanKey,
hasGuildPermission,
removeDojoKeyItems
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
@ -47,7 +41,14 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
// Update inventory of new member // Update inventory of new member
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string); inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory, inventoryChanges); const recipeChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
await inventory.save(); await inventory.save();
const guild = (await Guild.findById(req.query.clanId as string))!; const guild = (await Guild.findById(req.query.clanId as string))!;
@ -62,7 +63,7 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
await guild.save(); await guild.save();
res.json({ res.json({
...(await getGuildClient(guild, account)), ...(await getGuildClient(guild, account._id.toString())),
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges
}); });
} else { } else {
@ -95,9 +96,14 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req,
await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 }); await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
// Update inventory of new member // Update inventory of new member
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes"); const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string); inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory); addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
await inventory.save(); await inventory.save();
// Add join to clan log // Add join to clan log

View File

@ -2,7 +2,7 @@ import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService"; import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
@ -36,7 +36,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
// Either way, endo is given to the contributor. // Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints"); const inventory = await getInventory(accountId, "FusionPoints");
addFusionPoints(inventory, guild.CeremonyEndo!); inventory.FusionPoints += guild.CeremonyEndo!;
await inventory.save(); await inventory.save();
res.json({ res.json({

View File

@ -1,17 +1,16 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { createUniqueClanName, getGuildClient, giveClanKey } from "@/src/services/guildService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { addRecipes, getInventory } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const createGuildController: RequestHandler = async (req, res) => { export const createGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<ICreateGuildRequest>(String(req.body)); const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
// Remove pending applications for this account // Remove pending applications for this account
await GuildMember.deleteMany({ accountId: account._id, status: 1 }); await GuildMember.deleteMany({ accountId, status: 1 });
// Create guild on database // Create guild on database
const guild = new Guild({ const guild = new Guild({
@ -21,21 +20,32 @@ export const createGuildController: RequestHandler = async (req, res) => {
// Create guild member on database // Create guild member on database
await GuildMember.insertOne({ await GuildMember.insertOne({
accountId: account._id, accountId: accountId,
guildId: guild._id, guildId: guild._id,
status: 0, status: 0,
rank: 0 rank: 0
}); });
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes"); const inventory = await getInventory(accountId, "GuildId Recipes");
inventory.GuildId = guild._id; inventory.GuildId = guild._id;
const inventoryChanges: IInventoryChanges = {}; addRecipes(inventory, [
giveClanKey(inventory, inventoryChanges); {
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
await inventory.save(); await inventory.save();
res.json({ res.json({
...(await getGuildClient(guild, account)), ...(await getGuildClient(guild, accountId)),
InventoryChanges: inventoryChanges InventoryChanges: {
Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
}); });
}; };

View File

@ -1,5 +1,4 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
@ -8,23 +7,15 @@ import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => { export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers NemesisHistory"); const inventory = await getInventory(accountId, "CrewMembers");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body)); const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
if (data.crewMember.SecondInCommand) { const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
clearOnCall(inventory); dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
} dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
if (data.crewMember.ItemId.$oid == "000000000000000000000000") { dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!; dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand; dbCrewMember.Configs = data.crewMember.Configs;
} else { dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
}
await inventory.save(); await inventory.save();
res.json({ res.json({
crewMemberId: data.crewMember.ItemId.$oid, crewMemberId: data.crewMember.ItemId.$oid,
@ -35,20 +26,3 @@ export const crewMembersController: RequestHandler = async (req, res) => {
interface ICrewMembersRequest { interface ICrewMembersRequest {
crewMember: ICrewMemberClient; crewMember: ICrewMemberClient;
} }
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
if (inventory.NemesisHistory) {
for (const cm of inventory.NemesisHistory) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
}
};

View File

@ -1,107 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { ICrewShipComponentFingerprint, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
export const crewShipFusionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
const inventoryChanges: IInventoryChanges = {};
// Charge partial repair cost if fusing with an identified but unrepaired part
if (payload.SourceRecipe) {
const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
// Remove inferior item
if (payload.SourceRecipe) {
inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
} else {
const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
inventory[category].pull({ _id: inferiorId.$oid });
inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
}
// Upgrade superior item
const superiorItem = tierA < tierB ? itemB : itemA;
const inferiorItem = tierA < tierB ? itemA : itemB;
const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
superiorItem.UpgradeFingerprint!
) as ICrewShipComponentFingerprint;
const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
: { compat: "", buffs: [] };
if (isWeapon) {
for (let i = 0; i != fingerprint.buffs.length; ++i) {
const buffA = fingerprint.buffs[i];
const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
const fvalA = buffA.Value / 0x3fffffff;
const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
const percA = 0.3 + fvalA * (0.6 - 0.3);
const percB = 0.3 + fvalB * (0.6 - 0.3);
const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
const newFval = (newPerc - 0.3) / (0.6 - 0.3);
buffA.Value = Math.trunc(newFval * 0x3fffffff);
}
} else {
const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
const buffA = fingerprint.buffs[i];
const buffB = inferiorFingerprint.buffs[i];
const fvalA = buffA.Value / 0x3fffffff;
const fvalB = buffB.Value / 0x3fffffff;
const rangeA = superiorMeta[i].range;
const rangeB = inferiorMeta[i].range;
const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
buffA.Value = Math.trunc(newFval * 0x3fffffff);
}
if (inferiorFingerprint.SubroutineIndex) {
const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
if (!useSuperiorSubroutine) {
fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
}
}
}
superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inventoryChanges[category] = [superiorItem.toJSON() as any];
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipFusionRequest {
PartA: IOid;
PartB: IOid;
SourceRecipe: string;
UseSubroutineA: boolean;
}
const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02];

View File

@ -62,7 +62,14 @@ export const crewShipIdentifySalvageController: RequestHandler = async (req, res
} satisfies IInnateDamageFingerprint) } satisfies IInnateDamageFingerprint)
}; };
} }
addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges); addEquipment(
inventory,
"CrewShipSalvagedWeapons",
payload.ItemType,
undefined,
inventoryChanges,
defaultOverwrites
);
} }
inventoryChanges.CrewShipRawSalvage = [ inventoryChanges.CrewShipRawSalvage = [

View File

@ -1,529 +1,60 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const endlessXpController: RequestHandler = async (req, res) => { export const endlessXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IEndlessXpRequest>(String(req.body)); const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
if (payload.Mode == "r") {
const inventory = await getInventory(accountId, "EndlessXP");
inventory.EndlessXP ??= [];
let entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (!entry) {
entry = {
Category: payload.Category,
Earn: 0,
Claim: 0,
Choices: payload.Choices,
PendingRewards: []
};
inventory.EndlessXP.push(entry);
}
const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000; inventory.EndlessXP ??= [];
const weekEnd = weekStart + 604800000; const entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (entry) {
entry.Earn = 0;
entry.Claim = 0;
entry.BonusAvailable = new Date(weekStart);
entry.Expiry = new Date(weekEnd);
entry.Choices = payload.Choices; entry.Choices = payload.Choices;
entry.PendingRewards =
payload.Category == "EXC_HARD"
? generateHardModeRewards(payload.Choices)
: generateNormalModeRewards(payload.Choices);
await inventory.save();
res.json({
NewProgress: inventory.toJSON<IInventoryClient>().EndlessXP!.find(x => x.Category == payload.Category)!
});
} else if (payload.Mode == "c") {
const inventory = await getInventory(accountId);
const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!;
const inventoryChanges: IInventoryChanges = {};
for (const reward of entry.PendingRewards) {
if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) {
combineInventoryChanges(
inventoryChanges,
(
await handleStoreItemAcquisition(
reward.Rewards[0].StoreItem,
inventory,
reward.Rewards[0].ItemCount
)
).InventoryChanges
);
}
}
entry.Claim = entry.Earn;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
ClaimedXp: entry.Claim
});
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); inventory.EndlessXP.push({
throw new Error(`unexpected endlessXp mode: ${payload.Mode}`); Category: payload.Category,
Choices: payload.Choices
});
} }
}; await inventory.save();
type IEndlessXpRequest = res.json({
| { NewProgress: {
Mode: "r"; Category: payload.Category,
Category: TEndlessXpCategory; Earn: 0,
Choices: string[]; Claim: 0,
} BonusAvailable: {
| { $date: {
Mode: "c" | "something else"; $numberLong: "9999999999999"
Category: TEndlessXpCategory;
};
const generateRandomRewards = (deckName: string): ICountedStoreItem[] => {
const reward = getRandomElement(ExportRewards[deckName][0])!;
return [
{
StoreItem: reward.type,
ItemCount: reward.itemCount
}
];
};
const normalModeChosenRewards: Record<string, string[]> = {
Excalibur: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint"
],
Trinity: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint"
],
Ember: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint"
],
Loki: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint"
],
Mag: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint"
],
Rhino: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint"
],
Ash: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint"
],
Frost: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint"
],
Nyx: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint"
],
Saryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint"
],
Vauban: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint"
],
Nova: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint"
],
Nekros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint"
],
Valkyr: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint"
],
Oberon: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint"
],
Hydroid: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint"
],
Mirage: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint"
],
Limbo: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint"
],
Mesa: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint"
],
Chroma: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint"
],
Atlas: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint"
],
Ivara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint"
],
Inaros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint"
],
Titania: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint"
],
Nidus: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint"
],
Octavia: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint"
],
Harrow: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint"
],
Gara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint"
],
Khora: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint"
],
Revenant: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint"
],
Garuda: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint"
],
Baruuk: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint"
],
Hildryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint",
"/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint"
]
};
const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => {
const choiceRewards = normalModeChosenRewards[choices[0]];
return [
{
RequiredTotalXp: 190,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 400,
Rewards: [
{
StoreItem: choiceRewards[0],
ItemCount: 1
} }
] },
}, Expiry: {
{ $date: {
RequiredTotalXp: 630, $numberLong: "9999999999999"
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 890,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards"
)
},
{
RequiredTotalXp: 1190,
Rewards: [
{
StoreItem: choiceRewards[1],
ItemCount: 1
} }
] },
}, Choices: payload.Choices,
{ PendingRewards: [
RequiredTotalXp: 1540,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards"
)
},
{
RequiredTotalXp: 1950,
Rewards: [
{ {
StoreItem: choiceRewards[2], RequiredTotalXp: 190,
ItemCount: 1 Rewards: [
} {
] StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod",
}, ItemCount: 1
{ }
RequiredTotalXp: 2430, ]
Rewards: [
{
StoreItem: choiceRewards[3],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2990,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards"
)
},
{
RequiredTotalXp: 3640,
Rewards: [
{
StoreItem: choiceRewards[4],
ItemCount: 1
} }
// ...
] ]
} }
]; });
}; };
const hardModeChosenRewards: Record<string, string> = { interface IEndlessXpRequest {
Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker", Mode: string; // "r"
Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker", Category: TEndlessXpCategory;
Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker", Choices: string[];
Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker", }
Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
};
const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
return [
{
RequiredTotalXp: 285,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 600,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 945,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1335,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1785,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[0]],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2310,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 2925,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 3645,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 4485,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
)
},
{
RequiredTotalXp: 5460,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[1]],
ItemCount: 1
}
]
}
];
};

View File

@ -21,12 +21,10 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
inventory.EntratiVaultCountResetDate = new Date(weekEnd); inventory.EntratiVaultCountResetDate = new Date(weekEnd);
if (inventory.EntratiLabConquestUnlocked) { if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestUnlocked = 0; inventory.EntratiLabConquestUnlocked = 0;
inventory.EntratiLabConquestCacheScoreMission = 0;
inventory.EntratiLabConquestActiveFrameVariants = []; inventory.EntratiLabConquestActiveFrameVariants = [];
} }
if (inventory.EchoesHexConquestUnlocked) { if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestUnlocked = 0; inventory.EchoesHexConquestUnlocked = 0;
inventory.EchoesHexConquestCacheScoreMission = 0;
inventory.EchoesHexConquestActiveFrameVariants = []; inventory.EchoesHexConquestActiveFrameVariants = [];
inventory.EchoesHexConquestActiveStickers = []; inventory.EchoesHexConquestActiveStickers = [];
} }

View File

@ -30,14 +30,15 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 }); miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding); let affiliationMod;
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
MiscItems: miscItemChanges MiscItems: miscItemChanges
}, },
SyndicateTag: syndicateTag, SyndicateTag: syndicateTag,
StandingChange: gainedStanding StandingChange: affiliationMod?.Standing || 0
}); });
}; };

View File

@ -43,7 +43,7 @@ export const focusController: RequestHandler = async (req, res) => {
inventory.FocusAbility ??= focusType; inventory.FocusAbility ??= focusType;
inventory.FocusUpgrades.push({ ItemType: focusType }); inventory.FocusUpgrades.push({ ItemType: focusType });
if (inventory.FocusXP) { if (inventory.FocusXP) {
inventory.FocusXP[focusPolarity]! -= cost; inventory.FocusXP[focusPolarity] -= cost;
} }
await inventory.save(); await inventory.save();
res.json({ res.json({
@ -64,9 +64,7 @@ export const focusController: RequestHandler = async (req, res) => {
} }
); );
res.json({ res.end();
FocusUpgrade: { ItemType: focusType }
});
break; break;
} }
case FocusOperation.UnlockUpgrade: { case FocusOperation.UnlockUpgrade: {
@ -78,7 +76,7 @@ export const focusController: RequestHandler = async (req, res) => {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost; cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 }); inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
} }
inventory.FocusXP![focusPolarity]! -= cost; inventory.FocusXP![focusPolarity] -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusTypes: request.FocusTypes, FocusTypes: request.FocusTypes,
@ -96,7 +94,7 @@ export const focusController: RequestHandler = async (req, res) => {
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!; const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
focusUpgradeDb.Level = focusUpgrade.Level; focusUpgradeDb.Level = focusUpgrade.Level;
} }
inventory.FocusXP![focusPolarity]! -= cost; inventory.FocusXP![focusPolarity] -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusInfos: request.FocusInfos, FocusInfos: request.FocusInfos,
@ -106,14 +104,13 @@ export const focusController: RequestHandler = async (req, res) => {
} }
case FocusOperation.SentTrainingAmplifier: { case FocusOperation.SentTrainingAmplifier: {
const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest; const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
const parts: string[] = [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
];
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, { const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
]
});
occupySlot(inventory, InventorySlot.AMPS, false); occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save(); await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);
@ -123,7 +120,7 @@ export const focusController: RequestHandler = async (req, res) => {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest; const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length; inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
@ -168,10 +165,8 @@ export const focusController: RequestHandler = async (req, res) => {
shard.ItemCount *= -1; shard.ItemCount *= -1;
} }
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const polarity = request.Polarity; inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 };
inventory.FocusXP ??= {}; inventory.FocusXP[request.Polarity] += xp;
inventory.FocusXP[polarity] ??= 0;
inventory.FocusXP[polarity] += xp;
addMiscItems(inventory, request.Shards); addMiscItems(inventory, request.Shards);
await inventory.save(); await inventory.save();
break; break;

View File

@ -1,84 +0,0 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
import { toStoreItem } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IMissionReward } from "@/src/types/missionTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IGardeningClient } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { dict_en, ExportResources } from "warframe-public-export-plus";
export const gardeningController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGardeningRequest>(String(req.body));
if (data.Mode != "HarvestAll") {
throw new Error(`unexpected gardening mode: ${data.Mode}`);
}
const accountId = await getAccountIdForRequest(req);
const [inventory, personalRooms] = await Promise.all([
getInventory(accountId, "MiscItems"),
getPersonalRooms(accountId, "Apartment")
]);
// Harvest plants
const inventoryChanges: IInventoryChanges = {};
const rewards: Record<string, IMissionReward[][]> = {};
for (const planter of personalRooms.Apartment.Gardening.Planters) {
rewards[planter.Name] = [];
for (const plant of planter.Plants) {
const itemType =
"/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" +
plant.PlantType.substring(plant.PlantType.length - 1);
const itemCount = Math.random() < 0.775 ? 2 : 4;
addMiscItem(inventory, itemType, itemCount, inventoryChanges);
rewards[planter.Name].push([
{
StoreItem: toStoreItem(itemType),
TypeName: itemType,
ItemCount: itemCount,
DailyCooldown: false,
Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564,
TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`,
ProductCategory: "MiscItems"
}
]);
}
}
// Refresh garden
personalRooms.Apartment.Gardening = createGarden();
await Promise.all([inventory.save(), personalRooms.save()]);
const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1];
const plant = planter.Plants[planter.Plants.length - 1];
res.json({
GardenTagName: planter.Name,
PlantType: plant.PlantType,
PlotIndex: plant.PlotIndex,
EndTime: toMongoDate(plant.EndTime),
InventoryChanges: inventoryChanges,
Gardening: personalRooms.toJSON<IPersonalRoomsClient>().Apartment.Gardening,
Rewards: rewards
} satisfies IGardeningResponse);
};
interface IGardeningRequest {
Mode: string;
}
interface IGardeningResponse {
GardenTagName: string;
PlantType: string;
PlotIndex: number;
EndTime: IMongoDate;
InventoryChanges: IInventoryChanges;
Gardening: IGardeningClient;
Rewards: Record<string, IMissionReward[][]>;
}

View File

@ -18,7 +18,6 @@ export const getAllianceController: RequestHandler = async (req, res) => {
res.end(); res.end();
}; };
// POST request since U27
/*interface IGetAllianceRequest { /*interface IGetAllianceRequest {
memberCount: number; memberCount: number;
clanLeaderName: string; clanLeaderName: string;

View File

@ -1,54 +1,15 @@
import { toOid } from "@/src/helpers/inventoryHelpers"; import { Request, Response } from "express";
import { Friendship } from "@/src/models/friendModel";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "@/src/services/friendService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes";
import { Request, RequestHandler, Response } from "express";
// POST with {} instead of GET as of 38.5.0 // POST with {} instead of GET as of 38.5.0
export const getFriendsController: RequestHandler = async (req: Request, res: Response) => { const getFriendsController = (_request: Request, response: Response): void => {
const accountId = await getAccountIdForRequest(req); response.writeHead(200, {
const response: IGetFriendsResponse = { //Connection: "keep-alive",
Current: [], //"Content-Encoding": "gzip",
IncomingFriendRequests: [], "Content-Type": "text/html",
OutgoingFriendRequests: [] // charset: "UTF - 8",
}; "Content-Length": "3"
const [internalFriendships, externalFriendships] = await Promise.all([ });
Friendship.find({ owner: accountId }), response.end(Buffer.from([0x7b, 0x7d, 0x0a]));
Friendship.find({ friend: accountId }, "owner Note")
]);
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
response.IncomingFriendRequests.push({
_id: toOid(externalFriendship.owner),
Note: externalFriendship.Note
});
}
}
for (const internalFriendship of internalFriendships) {
const friendInfo: IFriendInfo = {
_id: toOid(internalFriendship.friend)
};
if (externalFriendships.find(x => x.owner.equals(internalFriendship.friend))) {
response.Current.push(friendInfo);
} else {
response.OutgoingFriendRequests.push(friendInfo);
}
}
const promises: Promise<void>[] = [];
for (const arr of Object.values(response)) {
for (const friendInfo of arr) {
promises.push(addAccountDataToFriendInfo(friendInfo));
promises.push(addInventoryDataToFriendInfo(friendInfo));
}
}
await Promise.all(promises);
res.json(response);
}; };
// interface IGetFriendsResponse { export { getFriendsController };
// Current: IFriendInfo[];
// IncomingFriendRequests: IFriendInfo[];
// OutgoingFriendRequests: IFriendInfo[];
// }
type IGetFriendsResponse = Record<"Current" | "IncomingFriendRequests" | "OutgoingFriendRequests", IFriendInfo[]>;

View File

@ -1,13 +1,13 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
export const getGuildController: RequestHandler = async (req, res) => { export const getGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId"); const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) { if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId); const guild = await Guild.findById(inventory.GuildId);
if (guild) { if (guild) {
@ -24,7 +24,7 @@ export const getGuildController: RequestHandler = async (req, res) => {
guild.CeremonyResetDate = undefined; guild.CeremonyResetDate = undefined;
await guild.save(); await guild.save();
} }
res.json(await getGuildClient(guild, account)); res.json(await getGuildClient(guild, accountId));
return; return;
} }
} }

View File

@ -2,7 +2,6 @@ import { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getDojoClient } from "@/src/services/guildService"; import { getDojoClient } from "@/src/services/guildService";
import { Account } from "@/src/models/loginModel";
export const getGuildDojoController: RequestHandler = async (req, res) => { export const getGuildDojoController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId as string; const guildId = req.query.guildId as string;
@ -26,8 +25,7 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
} }
const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {}; const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {};
const account = await Account.findById(req.query.accountId as string); res.json(await getDojoClient(guild, 0, payload.ComponentId));
res.json(await getDojoClient(guild, 0, payload.ComponentId, account?.BuildLabel));
}; };
interface IGetGuildDojoRequest { interface IGetGuildDojoRequest {

View File

@ -1,7 +1,7 @@
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { Account, Ignore } from "@/src/models/loginModel"; import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes"; import { IFriendInfo } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils"; import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express"; import { RequestHandler } from "express";

View File

@ -2,24 +2,19 @@ import { RequestHandler } from "express";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes"; import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService"; import { getLoadout } from "@/src/services/loadoutService";
export const getShipController: RequestHandler = async (req, res) => { export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const personalRoomsDb = await getPersonalRooms(accountId); const personalRoomsDb = await getPersonalRooms(accountId);
const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
// Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future.
if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) {
personalRoomsDb.Apartment.Gardening = createGarden();
await personalRoomsDb.save();
}
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
const loadout = await getLoadout(accountId); const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = { const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId, ShipOwnerId: accountId,
@ -29,8 +24,8 @@ export const getShipController: RequestHandler = async (req, res) => {
ShipId: toOid(personalRoomsDb.activeShipId), ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: { ShipInterior: {
Colors: personalRooms.ShipInteriorColors, Colors: personalRooms.ShipInteriorColors,
ShipAttachments: { HOOD_ORNAMENT: "" }, ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: "" SkinFlavourItem: ship.SkinFlavourItem
}, },
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId) ? toOid(personalRooms.Ship.FavouriteLoadoutId)

View File

@ -1,29 +1,14 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { config } from "@/src/services/configService";
export const getVendorInfoController: RequestHandler = async (req, res) => { export const getVendorInfoController: RequestHandler = (req, res) => {
let manifest = getVendorManifestByTypeName(req.query.vendor as string); if (typeof req.query.vendor == "string") {
if (!manifest) { const manifest = getVendorManifestByTypeName(req.query.vendor);
throw new Error(`Unknown vendor: ${req.query.vendor as string}`); if (!manifest) {
} throw new Error(`Unknown vendor: ${req.query.vendor}`);
// For testing purposes, authenticating with this endpoint is optional here, but would be required on live.
if (req.query.accountId) {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
manifest = applyStandingToVendorManifest(inventory, manifest);
if (config.dev?.keepVendorsExpired) {
manifest = {
VendorInfo: {
...manifest.VendorInfo,
Expiry: { $date: { $numberLong: "0" } }
}
};
} }
res.json(manifest);
} else {
res.status(400).end();
} }
res.json(manifest);
}; };

View File

@ -1,23 +1,16 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { areFriends } from "@/src/services/friendService";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { import { getInventory, updateCurrency } from "@/src/services/inventoryService";
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { IInventoryChanges, IPurchaseParams, PurchaseSource } from "@/src/types/purchaseTypes"; import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus"; import { ExportFlavour } from "warframe-public-export-plus";
export const giftingController: RequestHandler = async (req, res) => { export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body)); const data = getJSONfromString<IGiftingRequest>(String(req.body));
if (data.PurchaseParams.Source != PurchaseSource.Market || !data.PurchaseParams.UsePremium) { if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) {
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`); throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
} }
@ -37,11 +30,8 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
// Cannot gift to players who have gifting disabled. // Cannot gift to players who have gifting disabled.
const senderAccount = await getAccountForRequest(req); // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
if ( if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") {
inventory.Settings?.GiftMode == "GIFT_MODE_NONE" ||
(inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).send("17").end(); res.status(400).send("17").end();
return; return;
} }
@ -50,7 +40,11 @@ export const giftingController: RequestHandler = async (req, res) => {
// TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7)
// TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20) // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20)
const senderInventory = await getInventory(senderAccount._id.toString()); const senderAccount = await getAccountForRequest(req);
const senderInventory = await getInventory(
senderAccount._id.toString(),
"PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining"
);
if (senderInventory.GiftsRemaining == 0) { if (senderInventory.GiftsRemaining == 0) {
res.status(400).send("10").end(); res.status(400).send("10").end();
@ -58,20 +52,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
senderInventory.GiftsRemaining -= 1; senderInventory.GiftsRemaining -= 1;
const inventoryChanges: IInventoryChanges = updateCurrency( updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true);
senderInventory,
data.PurchaseParams.ExpectedPrice,
true
);
if (data.PurchaseParams.StoreItem in ExportBundles) {
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
if (bundle.giftingBonus) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
);
}
}
await senderInventory.save(); await senderInventory.save();
const senderName = getSuffixedName(senderAccount); const senderName = getSuffixedName(senderAccount);
@ -90,7 +71,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/GiftReceivedSubject", sub: "/Lotus/Language/Menu/GiftReceivedSubject",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
gifts: [ gifts: [
{ {
GiftType: data.PurchaseParams.StoreItem GiftType: data.PurchaseParams.StoreItem
@ -99,9 +80,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
]); ]);
res.json({ res.end();
InventoryChanges: inventoryChanges
});
}; };
interface IGiftingRequest { interface IGiftingRequest {

View File

@ -11,7 +11,7 @@ import {
scaleRequiredCount, scaleRequiredCount,
setGuildTechLogState setGuildTechLogState
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
addCrewShipWeaponSkin, addCrewShipWeaponSkin,
@ -104,7 +104,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
) { ) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
} }
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) { if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
throw new Error( throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array` `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
); );
@ -441,10 +441,16 @@ const finishComponentRepair = (
const inventoryChanges = { const inventoryChanges = {
...(category == "CrewShipWeaponSkins" ...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, { : addEquipment(
UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType, inventory,
UpgradeFingerprint: salvageItem.UpgradeFingerprint category,
})), salvageItem.ItemType,
undefined,
{},
{
UpgradeFingerprint: salvageItem.UpgradeFingerprint
}
)),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
}; };

View File

@ -1,24 +1,17 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { createNewSession } from "@/src/managers/sessionManager"; import { createNewSession } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ISession } from "@/src/types/session"; import { ISession } from "@/src/types/session";
import { JSONParse } from "json-with-bigint";
import { toOid2, version_compare } from "@/src/helpers/inventoryHelpers";
const hostSessionController: RequestHandler = async (req, res) => { const hostSessionController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const hostSessionRequest = JSONParse(String(req.body)) as ISession; const hostSessionRequest = JSON.parse(req.body as string) as ISession;
logger.debug("HostSession Request", { hostSessionRequest }); logger.debug("HostSession Request", { hostSessionRequest });
const session = createNewSession(hostSessionRequest, account._id); const session = createNewSession(hostSessionRequest, accountId);
logger.debug(`New Session Created`, { session }); logger.debug(`New Session Created`, { session });
if (account.BuildLabel && version_compare(account.BuildLabel, "2015.03.21.08.17") < 0) { res.json({ sessionId: { $oid: session.sessionId }, rewardSeed: 99999999 });
// U15 or below
res.send(session.sessionId.toString());
} else {
res.json({ sessionId: toOid2(session.sessionId, account.BuildLabel), rewardSeed: 99999999 });
}
}; };
export { hostSessionController }; export { hostSessionController };

View File

@ -9,17 +9,10 @@ import {
getMessage getMessage
} from "@/src/services/inboxService"; } from "@/src/services/inboxService";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
addItems,
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus"; import { ExportFlavour, ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
export const inboxController: RequestHandler = async (req, res) => { export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
@ -34,10 +27,10 @@ export const inboxController: RequestHandler = async (req, res) => {
return; return;
} }
await deleteMessageRead(parseOid(deleteId as string)); await deleteMessageRead(deleteId as string);
res.status(200).end(); res.status(200).end();
} else if (messageId) { } else if (messageId) {
const message = await getMessage(parseOid(messageId as string)); const message = await getMessage(messageId as string);
message.r = true; message.r = true;
await message.save(); await message.save();
@ -55,8 +48,8 @@ export const inboxController: RequestHandler = async (req, res) => {
await addItems( await addItems(
inventory, inventory,
attachmentItems.map(attItem => ({ attachmentItems.map(attItem => ({
ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem, ItemType: attItem,
ItemCount: 1 ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})), })),
inventoryChanges inventoryChanges
); );
@ -93,7 +86,7 @@ export const inboxController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject", sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
icon: ExportFlavour[getEffectiveAvatarImageType(inventory)].icon, icon: ExportFlavour[inventory.ActiveAvatarImageType].icon,
highPriority: true highPriority: true
} }
]); ]);
@ -106,7 +99,7 @@ export const inboxController: RequestHandler = async (req, res) => {
await createNewEventMessages(req); await createNewEventMessages(req);
const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 }); const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 });
const latestClientMessage = messages.find(m => m._id.toString() === parseOid(latestClientMessageId as string)); const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId);
if (!latestClientMessage) { if (!latestClientMessage) {
logger.debug(`this should only happen after DeleteAllRead `); logger.debug(`this should only happen after DeleteAllRead `);
@ -129,11 +122,3 @@ export const inboxController: RequestHandler = async (req, res) => {
res.json({ Inbox: inbox }); res.json({ Inbox: inbox });
} }
}; };
// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb
const parseOid = (oid: string): string => {
if (oid[0] == "{") {
return (JSON.parse(oid) as IOid).$oid;
}
return oid;
};

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
@ -12,7 +12,7 @@ import {
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc } from "warframe-public-export-plus"; import { ExportMisc } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper"; import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
@ -23,16 +23,15 @@ import {
} from "@/src/services/infestedFoundryService"; } from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
switch (req.query.mode) { switch (req.query.mode) {
case "s": { case "s": {
// shard installation // shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
suit.ArchonCrystalUpgrades ??= []; if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
while (suit.ArchonCrystalUpgrades.length < request.Slot) { suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
suit.ArchonCrystalUpgrades.push({});
} }
suit.ArchonCrystalUpgrades[request.Slot] = { suit.ArchonCrystalUpgrades[request.Slot] = {
UpgradeType: request.UpgradeType, UpgradeType: request.UpgradeType,
@ -57,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "x": { case "x": {
// shard removal // shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body)); const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
@ -71,30 +70,18 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
ItemCount: 1 ItemCount: 1
}); });
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
// consume resources
if (!config.infiniteHelminthMaterials) {
let type: string;
let count: number;
if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
// < 38.6.0
type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile";
count = 300;
} else {
// >= 38.6.0
type =
archonCrystalRemovalResource[
suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "")
];
count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150;
}
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count;
}
} }
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined; suit.ArchonCrystalUpgrades![request.Slot] = {};
suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
if (!config.infiniteHelminthMaterials) {
// remove bile
const bile = inventory.InfestedFoundry!.Resources!.find(
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
)!;
bile.Count -= 300;
}
await inventory.save(); await inventory.save();
@ -112,7 +99,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "n": { case "n": {
// name the beast // name the beast
const request = getJSONfromString<IHelminthNameRequest>(String(req.body)); const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Name = request.newName; inventory.InfestedFoundry.Name = request.newName;
await inventory.save(); await inventory.save();
@ -135,7 +122,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body)); const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Resources ??= []; inventory.InfestedFoundry.Resources ??= [];
@ -231,7 +218,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "o": { case "o": {
// offerings update // offerings update
const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body)); const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex; inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes; inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
@ -252,7 +239,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "a": { case "a": {
// subsume warframe // subsume warframe
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body)); const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const recipe = getRecipe(request.Recipe)!; const recipe = getRecipe(request.Recipe)!;
if (!config.infiniteHelminthMaterials) { if (!config.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) { for (const ingredient of recipe.secretIngredients!) {
@ -302,7 +289,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "r": { case "r": {
// rush subsume // rush subsume
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 50, true); const currencyChanges = updateCurrency(inventory, 50, true);
const recipeChanges = handleSubsumeCompletion(inventory); const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save(); await inventory.save();
@ -320,7 +307,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "u": { case "u": {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body)); const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.OffensiveUpgrade = request.OffensiveUpgradeType;
@ -353,7 +340,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
case "custom_unlockall": { case "custom_unlockall": {
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.XP ??= 0; inventory.InfestedFoundry.XP ??= 0;
if (151875_00 > inventory.InfestedFoundry.XP) { if (151875_00 > inventory.InfestedFoundry.XP) {
@ -452,12 +439,3 @@ const apetiteModel = (x: number): number => {
} }
return 3; return 3;
}; };
const archonCrystalRemovalResource: Record<string, string> = {
ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides",
ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile",
ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics",
ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics",
ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones",
ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx"
};

View File

@ -1,34 +1,32 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json"; import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus"; import {
ExportCustoms,
ExportFlavour,
ExportRegions,
ExportResources,
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { import {
addMiscItems, addMiscItems,
allDailyAffiliationKeys, allDailyAffiliationKeys,
cleanupInventory, cleanupInventory,
createLibraryDailyTask, createLibraryDailyTask
generateRewardSeed
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { addString, catBreadHash } from "@/src/helpers/stringHelpers"; import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
import { Inbox } from "@/src/models/inboxModel";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const accountId = await getAccountIdForRequest(request);
const inventory = await Inventory.findOne({ accountOwnerId: account._id }); const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (!inventory) { if (!inventory) {
response.status(400).json({ error: "inventory was undefined" }); response.status(400).json({ error: "inventory was undefined" });
@ -89,7 +87,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
cleanupInventory(inventory); cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
//await inventory.save(); await inventory.save();
} }
if ( if (
@ -98,51 +96,23 @@ export const inventoryController: RequestHandler = async (request, response) =>
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) { ) {
handleSubsumeCompletion(inventory); handleSubsumeCompletion(inventory);
//await inventory.save(); await inventory.save();
} }
if (inventory.LastInventorySync) { response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
if (!inventory.DuviriInfo) {
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
} else {
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save();
response.json(
await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query, account.BuildLabel)
);
}; };
export const getInventoryResponse = async ( export const getInventoryResponse = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
xpBasedLevelCapDisabled: boolean, xpBasedLevelCapDisabled: boolean
buildLabel: string | undefined
): Promise<IInventoryClient> => { ): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([ const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>(
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), "LoadOutPresets"
Ship.find({ ShipOwnerId: inventory.accountOwnerId }), );
Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 }) const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>(
]); "Ships"
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>(); );
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>()); const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON<IInventoryClient>();
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
if (latestMessage) {
inventoryResponse.Mailbox = {
LastInboxId: toOid(latestMessage._id)
};
}
if (config.infiniteCredits) { if (config.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
@ -170,6 +140,18 @@ export const getInventoryResponse = async (
} }
} }
if (config.unlockAllMissions) {
inventoryResponse.Missions = [];
for (const tag of Object.keys(ExportRegions)) {
inventoryResponse.Missions.push({
Completes: 1,
Tier: 1,
Tag: tag
});
}
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
}
if (config.unlockAllShipDecorations) { if (config.unlockAllShipDecorations) {
inventoryResponse.ShipDecorations = []; inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) { for (const [uniqueName, item] of Object.entries(ExportResources)) {
@ -292,79 +274,19 @@ export const getInventoryResponse = async (
} }
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
inventoryResponse.LastInventorySync = undefined; //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// Set 2FA enabled so trading post can be used // Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true; inventoryResponse.HWIDProtectEnabled = true;
if (buildLabel) {
// Fix nemesis for older versions
if (
inventoryResponse.Nemesis &&
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
) {
inventoryResponse.Nemesis = undefined;
}
if (version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
// U19.5 and below use $id instead of $oid
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
}
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
}
}
}
if (config.unlockAllProfitTakerStages) {
inventoryResponse.CompletedJobChains ??= [];
const EudicoHeists = inventoryResponse.CompletedJobChains.find(x => x.LocationTag == "EudicoHeists");
if (EudicoHeists) {
EudicoHeists.Jobs = allEudicoHeistJobs;
} else {
inventoryResponse.CompletedJobChains.push({
LocationTag: "EudicoHeists",
Jobs: allEudicoHeistJobs
});
}
}
if (config.unlockAllSimarisResearchEntries) {
inventoryResponse.LibraryPersonalTarget = undefined;
inventoryResponse.LibraryPersonalProgress = [
"/Lotus/Types/Game/Library/Targets/Research1Target",
"/Lotus/Types/Game/Library/Targets/Research2Target",
"/Lotus/Types/Game/Library/Targets/Research3Target",
"/Lotus/Types/Game/Library/Targets/Research4Target",
"/Lotus/Types/Game/Library/Targets/Research5Target",
"/Lotus/Types/Game/Library/Targets/Research6Target",
"/Lotus/Types/Game/Library/Targets/Research7Target"
].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
}
return inventoryResponse; return inventoryResponse;
}; };
const allEudicoHeistJobs = [ const addString = (arr: string[], str: string): void => {
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne", if (!arr.find(x => x == str)) {
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyTwo", arr.push(str);
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree", }
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyFour" };
];
const getExpRequiredForMr = (rank: number): number => { const getExpRequiredForMr = (rank: number): number => {
if (rank <= 30) { if (rank <= 30) {

View File

@ -4,57 +4,48 @@ import { config } from "@/src/services/configService";
import { buildConfig } from "@/src/services/buildConfigService"; import { buildConfig } from "@/src/services/buildConfigService";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { sendWsBroadcastTo } from "@/src/services/webService";
export const loginController: RequestHandler = async (request, response) => { 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 const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
const account = await Account.findOne({ email: loginRequest.email }); const account = await Account.findOne({ email: loginRequest.email });
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
const buildLabel: string = const buildLabel: string =
typeof request.query.buildLabel == "string" typeof request.query.buildLabel == "string"
? request.query.buildLabel.split(" ").join("+") ? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel; : buildConfig.buildLabel;
let myAddress: string; const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
let myUrlBase: string = request.protocol + "://";
if (request.host.indexOf("warframe.com") == -1) {
// Client request was redirected cleanly, so we know it can reach us how it's reaching us now.
myAddress = request.hostname;
myUrlBase += request.host;
} else {
// Don't know how the client reached us, hoping the config does.
myAddress = config.myAddress;
myUrlBase += myAddress;
const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443;
if (port != (request.protocol == "http" ? 80 : 443)) {
myUrlBase += ":" + port;
}
}
if ( if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
!account &&
((config.autoCreateAccount && loginRequest.ClientType != "webui") ||
loginRequest.ClientType == "webui-register")
) {
try { try {
const name = await getUsernameFromEmail(loginRequest.email); const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
if (await isNameTaken(name)) {
let suffix = 0;
do {
++suffix;
name = nameFromEmail + suffix;
} while (await isNameTaken(name));
}
const newAccount = await createAccount({ const newAccount = await createAccount({
email: loginRequest.email, email: loginRequest.email,
password: loginRequest.password, password: loginRequest.password,
DisplayName: name, DisplayName: name,
CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType, ClientType: loginRequest.ClientType,
Nonce: createNonce(), CrossPlatformAllowed: true,
BuildLabel: buildLabel, ForceLogoutVersion: 0,
LastLogin: new Date() ConsentNeeded: false,
TrackedSettings: [],
Nonce: nonce
}); });
logger.debug("created new account"); logger.debug("created new account");
response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel)); response.json(createLoginResponse(myAddress, newAccount, buildLabel));
return; return;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -73,75 +64,46 @@ export const loginController: RequestHandler = async (request, response) => {
return; return;
} }
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) { if (loginRequest.ClientType == "webui") {
// U17 seems to handle "nonce still set" like a login failure. if (!account.Nonce) {
if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) { account.ClientType = "webui";
response.status(400).send({ error: "nonce still set" }); account.Nonce = nonce;
}
} else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
response.status(400).json({ error: "nonce still set" });
return; return;
} }
}
account.ClientType = loginRequest.ClientType; account.ClientType = loginRequest.ClientType;
account.Nonce = createNonce(); account.Nonce = nonce;
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN"; account.CountryCode = loginRequest.lang.toUpperCase();
account.BuildLabel = buildLabel; }
account.LastLogin = new Date();
await account.save(); await account.save();
// Tell WebUI its nonce has been invalidated response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
sendWsBroadcastTo(account._id.toString(), { logged_out: true });
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
}; };
const createLoginResponse = ( const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
myAddress: string, return {
myUrlBase: string,
account: IDatabaseAccountJson,
buildLabel: string
): ILoginResponse => {
const resp: ILoginResponse = {
id: account.id, id: account.id,
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
CountryCode: account.CountryCode, CountryCode: account.CountryCode,
ClientType: account.ClientType,
CrossPlatformAllowed: account.CrossPlatformAllowed,
ForceLogoutVersion: account.ForceLogoutVersion,
AmazonAuthToken: account.AmazonAuthToken, AmazonAuthToken: account.AmazonAuthToken,
AmazonRefreshToken: account.AmazonRefreshToken, AmazonRefreshToken: account.AmazonRefreshToken,
ConsentNeeded: account.ConsentNeeded,
TrackedSettings: account.TrackedSettings,
Nonce: account.Nonce, Nonce: account.Nonce,
BuildLabel: buildLabel Groups: [],
IRC: config.myIrcAddresses ?? [myAddress],
platformCDNs: [`https://${myAddress}/`],
HUB: `https://${myAddress}/api/`,
NRS: config.NRS,
DTLS: 99,
BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
}; };
if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) {
resp.NRS = config.NRS;
}
if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) {
// U17 and up
resp.IRC = config.myIrcAddresses ?? [myAddress];
}
if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) {
// U24 and up
resp.ConsentNeeded = account.ConsentNeeded;
resp.TrackedSettings = account.TrackedSettings;
}
if (version_compare(buildLabel, "2019.08.29.20.01") >= 0) {
// U25.7 and up
resp.ForceLogoutVersion = account.ForceLogoutVersion;
}
if (version_compare(buildLabel, "2019.10.31.22.42") >= 0) {
// U26 and up
resp.Groups = [];
}
if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) {
resp.DTLS = 99;
}
if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) {
resp.ClientType = account.ClientType;
}
if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
resp.HUB = `${myUrlBase}/api/`;
resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
}
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
resp.platformCDNs = [`${myUrlBase}/`];
}
return resp;
}; };

View File

@ -26,7 +26,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
StoreItemType: body.ChosenReward StoreItemType: body.ChosenReward
}; };
inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges; inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
if (evergreenRewards.indexOf(body.ChosenReward) == -1) { if (!evergreenRewards.find(x => x == body.ChosenReward)) {
inventory.LoginMilestoneRewards.push(body.ChosenReward); inventory.LoginMilestoneRewards.push(body.ChosenReward);
} }
} else { } else {

View File

@ -1,6 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { sendWsBroadcastTo } from "@/src/services/webService";
export const logoutController: RequestHandler = async (req, res) => { export const logoutController: RequestHandler = async (req, res) => {
if (!req.query.accountId) { if (!req.query.accountId) {
@ -11,7 +10,7 @@ export const logoutController: RequestHandler = async (req, res) => {
throw new Error("Request is missing nonce parameter"); throw new Error("Request is missing nonce parameter");
} }
const stat = await Account.updateOne( await Account.updateOne(
{ {
_id: req.query.accountId, _id: req.query.accountId,
Nonce: nonce Nonce: nonce
@ -20,10 +19,6 @@ export const logoutController: RequestHandler = async (req, res) => {
Nonce: 0 Nonce: 0
} }
); );
if (stat.modifiedCount) {
// Tell WebUI its nonce has been invalidated
sendWsBroadcastTo(req.query.accountId as string, { logged_out: true });
}
res.writeHead(200, { res.writeHead(200, {
"Content-Type": "text/html", "Content-Type": "text/html",

View File

@ -1,13 +1,11 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService"; import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { generateRewardSeed, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController"; import { getInventoryResponse } from "./inventoryController";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
import { sendWsBroadcastTo } from "@/src/services/webService";
/* /*
**** INPUT **** **** INPUT ****
@ -50,51 +48,34 @@ import { sendWsBroadcastTo } from "@/src/services/webService";
*/ */
//move credit calc in here, return MissionRewards: [] if no reward info //move credit calc in here, return MissionRewards: [] if no reward info
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => { export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString()); const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString());
logger.debug("mission report:", missionReport); logger.debug("mission report:", missionReport);
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const firstCompletion = missionReport.SortieId const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false; : false;
const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if ( if (
missionReport.MissionStatus !== "GS_SUCCESS" && missionReport.MissionStatus !== "GS_SUCCESS" &&
!( !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
missionReport.RewardInfo?.jobId ||
missionReport.RewardInfo?.challengeMissionId ||
missionReport.RewardInfo?.T
)
) { ) {
if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed();
}
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({ res.json({
InventoryJson: JSON.stringify(inventoryResponse), InventoryJson: JSON.stringify(inventoryResponse),
MissionRewards: [] MissionRewards: []
}); });
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
return; return;
} }
const { const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } =
MissionRewards, await addMissionRewards(inventory, missionReport, firstCompletion);
inventoryChanges,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed();
}
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel); const inventoryResponse = await getInventoryResponse(inventory, true);
//TODO: figure out when to send inventory. it is needed for many cases. //TODO: figure out when to send inventory. it is needed for many cases.
res.json({ res.json({
@ -105,10 +86,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
...inventoryUpdates, ...inventoryUpdates,
//FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed. //FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed.
SyndicateXPItemReward, SyndicateXPItemReward,
AffiliationMods, AffiliationMods
ConquestCompletedMissionsCount });
} satisfies IMissionInventoryUpdateResponse);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
}; };
/* /*

View File

@ -17,7 +17,7 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService"; import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus"; import { ExportSentinels, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
interface IModularCraftRequest { interface IModularCraftRequest {
@ -36,9 +36,7 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
let defaultUpgrades: IDefaultUpgrade[] | undefined; let defaultUpgrades: IDefaultUpgrade[] | undefined;
const defaultOverwrites: Partial<IEquipmentDatabase> = { const defaultOverwrites: Partial<IEquipmentDatabase> = {};
ModularParts: data.Parts
};
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {};
if (category == "KubrowPets") { if (category == "KubrowPets") {
const traits = { const traits = {
@ -140,20 +138,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
} else { } else {
defaultUpgrades = getDefaultUpgrades(data.Parts); defaultUpgrades = getDefaultUpgrades(data.Parts);
} }
if (category == "MoaPets") {
const weapon = ExportSentinels[data.WeaponType].defaultWeapon;
if (weapon) {
const category = ExportWeapons[weapon].productCategory;
addEquipment(inventory, category, weapon, undefined, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
}
}
defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades); defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades);
addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges); addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites);
combineInventoryChanges( combineInventoryChanges(
inventoryChanges, inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi) occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)

View File

@ -2,7 +2,7 @@ import { RequestHandler } from "express";
import { ExportWeapons } from "warframe-public-export-plus"; import { ExportWeapons } from "warframe-public-export-plus";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { SRng } from "@/src/services/rngService"; import { CRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { import {
@ -21,11 +21,7 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => { export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {}; const partTypeToParts: Record<string, string[]> = {};
for (const [uniqueName, data] of Object.entries(ExportWeapons)) { for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if ( if (data.partType && data.premiumPrice) {
data.partType &&
data.premiumPrice &&
!data.excludeFromCodex // exclude pvp variants
) {
partTypeToParts[data.partType] ??= []; partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName); partTypeToParts[data.partType].push(uniqueName);
} }
@ -45,18 +41,24 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts); const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades); const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = { const inventoryChanges: IInventoryChanges = {
...addEquipment(inventory, category, weaponInfo.ItemType, { ...addEquipment(
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED, inventory,
ItemName: payload.ItemName, category,
Configs: configs, weaponInfo.ItemType,
ModularParts: weaponInfo.ModularParts, weaponInfo.ModularParts,
Polarity: [ {},
{ {
Slot: payload.PolarizeSlot, Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
Value: payload.PolarizeValue ItemName: payload.ItemName,
} Configs: configs,
] Polarity: [
}), {
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}
),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, true), ...occupySlot(inventory, productCategoryToInventoryBin(category)!, true),
...updateCurrency(inventory, weaponInfo.PremiumPrice, true) ...updateCurrency(inventory, weaponInfo.PremiumPrice, true)
}; };
@ -140,8 +142,8 @@ const getModularWeaponSale = (
partTypes: string[], partTypes: string[],
getItemType: (parts: string[]) => string getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => { ): IModularWeaponSaleInfo => {
const rng = new SRng(day); const rng = new CRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!); const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType]));
let partsCost = 0; let partsCost = 0;
for (const part of parts) { for (const part of parts) {
partsCost += ExportWeapons[part].premiumPrice!; partsCost += ExportWeapons[part].premiumPrice!;

View File

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

View File

@ -1,47 +1,33 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { import {
antivirusMods,
consumeModCharge, consumeModCharge,
decodeNemesisGuess,
encodeNemesisGuess, encodeNemesisGuess,
getInfNodes, getInfNodes,
getKnifeUpgrade,
getNemesisManifest,
getNemesisPasscode, getNemesisPasscode,
GUESS_CORRECT, IKnifeResponse
GUESS_INCORRECT,
GUESS_NEUTRAL,
GUESS_NONE,
GUESS_WILDCARD,
IKnifeResponse,
parseUpgrade
} from "@/src/helpers/nemesisHelpers"; } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { import {
IInnateDamageFingerprint, IInnateDamageFingerprint,
IInventoryClient,
INemesisClient,
InventorySlot, InventorySlot,
IUpgradeClient, IUpgradeClient,
IWeaponSkinClient, IWeaponSkinClient,
LoadoutIndex, LoadoutIndex,
TEquipmentKey, TEquipmentKey
TNemesisFaction
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const nemesisController: RequestHandler = async (req, res) => { export const nemesisController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
if ((req.query.mode as string) == "f") { if ((req.query.mode as string) == "f") {
const body = getJSONfromString<IValenceFusionRequest>(String(req.body)); const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
const inventory = await getInventory(account._id.toString(), body.Category + " WeaponBin"); const inventory = await getInventory(accountId, body.Category + " WeaponBin");
const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!; const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!;
const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!; const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!;
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
@ -76,7 +62,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
}); });
} else if ((req.query.mode as string) == "p") { } else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body)); const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!); const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0; let guessResult = 0;
@ -89,7 +75,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
} else { } else {
for (let i = 0; i != 3; ++i) { for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) { if (body.guess[i] == passcode[i]) {
++guessResult; ++guessResult;
} }
} }
@ -97,182 +83,114 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({ GuessResult: guessResult }); res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") { } else if (req.query.mode == "r") {
const inventory = await getInventory( const inventory = await getInventory(
account._id.toString(), accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
); );
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body)); const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") { if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT; // Add to GuessHistory
const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT; const result1 = passcode == guess[0] ? 0 : 1;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
inventory.Nemesis!.GuessHistory.push( inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess([ encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
{
symbol: guess[0],
result: result1
},
{
symbol: guess[1],
result: result2
},
{
symbol: guess[2],
result: result3
}
])
); );
// Increase antivirus if correct antivirus mod is installed // Increase antivirus
let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const response: IKnifeResponse = {}; const response: IKnifeResponse = {};
if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) { for (const upgrade of body.knife!.AttachedUpgrades) {
let antivirusGain = 5; switch (upgrade.ItemType) {
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); antivirusGain += 10;
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0; consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!; break;
for (const upgrade of body.knife!.AttachedUpgrades) { case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
switch (upgrade.ItemType) { antivirusGain += 10;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod": case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 10; antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break; break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield antivirusGain += 15;
antivirusGain += 15; consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); break;
break; case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
} antivirusGain += 10;
} consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
inventory.Nemesis!.HenchmenKilled += antivirusGain; break;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
// Weaken nemesis now.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
} }
} }
inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled < 100) { if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0); inventory.Nemesis!.HenchmenKilled = 100;
inventory.Nemesis!.InfNodes = [
{
Node: "CrewBattleNode559",
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
} else {
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
} }
await inventory.save(); await inventory.save();
res.json(response); res.json(response);
} else { } else {
// For first guess, create a new entry. const passcode = getNemesisPasscode(inventory.Nemesis!);
if (body.position == 0) { if (passcode[body.position] != body.guess) {
inventory.Nemesis!.GuessHistory.push( res.end();
encodeNemesisGuess([
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
},
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
},
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
}
])
);
}
// Evaluate guess
const correct =
body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess;
// Update entry
const guess = decodeNemesisGuess(
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1]
);
guess[body.position].symbol = body.guess;
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
const response: INemesisRequiemResponse = {};
if (correct) {
if (body.position == 2) {
// That was all 3 guesses correct, nemesis is now weakened.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
// Subtract a charge from all requiem mods installed on parazon
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(
inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid
);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
for (let i = 3; i != 6; ++i) {
//logger.debug(`subtracting a charge from ${dataknifeUpgrades[i]}`);
const upgrade = parseUpgrade(inventory, dataknifeUpgrades[i]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
}
} else { } else {
// Guess was incorrect, increase rank inventory.Nemesis!.Rank += 1;
response.RankIncrease = 1; inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
const manifest = getNemesisManifest(inventory.Nemesis!.manifest); await inventory.save();
inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1); res.json({ RankIncrease: 1 });
inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
} }
await inventory.save();
res.json(response);
} }
} else if ((req.query.mode as string) == "rs") { } else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body // report spawn; POST but no application data in body
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount; inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save(); await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc }); res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") { } else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
if (inventory.Nemesis) {
logger.warn(`overwriting an existing nemesis as a new one is being requested`);
}
const body = getJSONfromString<INemesisStartRequest>(String(req.body)); const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp); body.target.fp = BigInt(body.target.fp);
const manifest = getNemesisManifest(body.target.manifest);
if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) {
logger.warn(
`client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.`
);
}
let weaponIdx = -1; let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") { if (body.target.Faction != "FC_INFESTATION") {
const weapons: readonly string[] = manifest.weapons; let weapons: readonly string[];
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
weapons = kuvaLichVersionSixWeapons;
} else if (
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
) {
weapons = corpusVersionThreeWeapons;
} else {
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
}
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx; weaponIdx = initialWeaponIdx;
if (body.target.DisallowedWeapons) { do {
do { const weapon = weapons[weaponIdx];
const weapon = weapons[weaponIdx]; if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { break;
break; }
} weaponIdx = (weaponIdx + 1) % weapons.length;
weaponIdx = (weaponIdx + 1) % weapons.length; } while (weaponIdx != initialWeaponIdx);
} while (weaponIdx != initialWeaponIdx);
}
} }
inventory.Nemesis = { inventory.Nemesis = {
@ -289,14 +207,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
k: false, k: false,
Traded: false, Traded: false,
d: new Date(), d: new Date(),
InfNodes: getInfNodes(manifest, 0), InfNodes: getInfNodes(body.target.Faction, 0),
GuessHistory: [], GuessHistory: [],
Hints: [], Hints: [],
HintProgress: 0, HintProgress: 0,
Weakened: false, Weakened: body.target.Weakened,
PrevOwners: 0, PrevOwners: 0,
HenchmenKilled: 0, HenchmenKilled: 0,
SecondInCommand: false, SecondInCommand: body.target.SecondInCommand,
MissionCount: 0, MissionCount: 0,
LastEnc: 0 LastEnc: 0
}; };
@ -305,17 +223,6 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({ res.json({
target: inventory.toJSON().Nemesis target: inventory.toJSON().Nemesis
}); });
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(account._id.toString(), "Nemesis");
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
// As of 38.6.0, this request is no longer sent, instead mode=r already weakens the nemesis if appropriate.
// We always weaken the nemesis in mode=r so simply giving the client back the nemesis.
const response: INemesisWeakenResponse = {
target: inventory.toJSON<IInventoryClient>().Nemesis!
};
res.json(response);
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`); throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
@ -336,11 +243,11 @@ interface INemesisStartRequest {
KillingSuit: string; KillingSuit: string;
killingDamageType: number; killingDamageType: number;
ShoulderHelmet: string; ShoulderHelmet: string;
DisallowedWeapons?: string[]; DisallowedWeapons: string[];
WeaponIdx: number; WeaponIdx: number;
AgentIdx: number; AgentIdx: number;
BirthNode: string; BirthNode: string;
Faction: TNemesisFaction; Faction: string;
Rank: number; Rank: number;
k: boolean; k: boolean;
Traded: boolean; Traded: boolean;
@ -367,27 +274,48 @@ interface INemesisRequiemRequest {
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
position: number; // grn/crp: 0-2 | coda: 0 position: number; // grn/crp: 0-2 | coda: 0
// knife field provided for coda only // knife field provided for coda only
knife?: IKnife; knife?: {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
};
} }
interface INemesisRequiemResponse extends IKnifeResponse { const kuvaLichVersionSixWeapons = [
RankIncrease?: number; "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
} "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
// interface INemesisWeakenRequest { const corpusVersionThreeWeapons = [
// target: INemesisClient; "/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
// knife: IKnife; "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
// } "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
interface INemesisWeakenResponse extends IKnifeResponse { "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
target: INemesisClient; "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
} "/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
interface IKnife { "/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
Item: IEquipmentClient; "/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
Skins: IWeaponSkinClient[]; ];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
}

View File

@ -37,7 +37,6 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
const deco = component.Decos.find(x => x._id.equals(request.MoveId))!; const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
deco.Pos = request.Pos; deco.Pos = request.Pos;
deco.Rot = request.Rot; deco.Rot = request.Rot;
deco.Scale = request.Scale;
} else { } else {
const deco = const deco =
component.Decos[ component.Decos[
@ -46,7 +45,6 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
Type: request.Type, Type: request.Type,
Pos: request.Pos, Pos: request.Pos,
Rot: request.Rot, Rot: request.Rot,
Scale: request.Scale,
Name: request.Name, Name: request.Name,
Sockets: request.Sockets Sockets: request.Sockets
}) - 1 }) - 1
@ -115,9 +113,9 @@ interface IPlaceDecoInComponentRequest {
Type: string; Type: string;
Pos: number[]; Pos: number[];
Rot: number[]; Rot: number[];
Scale?: number;
Name?: string; Name?: string;
Sockets?: number; Sockets?: number;
Scale?: number; // only provided alongside MoveId and seems to always be 1
MoveId?: string; MoveId?: string;
ShipDeco?: boolean; ShipDeco?: boolean;
VaultDeco?: boolean; VaultDeco?: boolean;

View File

@ -2,16 +2,13 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { ExportRelics, IRelic } from "warframe-public-export-plus"; import { ExportRelics, IRelic } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const projectionManagerController: RequestHandler = async (req, res) => { export const projectionManagerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest; const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest;
const [era, category, currentQuality] = parseProjection(request.projectionType); const [era, category, currentQuality] = parseProjection(request.projectionType);
const upgradeCost = config.dontSubtractVoidTraces const upgradeCost = (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
? 0
: (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]); const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]);
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {

View File

@ -3,7 +3,6 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPurchaseRequest } from "@/src/types/purchaseTypes"; import { IPurchaseRequest } from "@/src/types/purchaseTypes";
import { handlePurchase } from "@/src/services/purchaseService"; import { handlePurchase } from "@/src/services/purchaseService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { sendWsBroadcastTo } from "@/src/services/webService";
export const purchaseController: RequestHandler = async (req, res) => { export const purchaseController: RequestHandler = async (req, res) => {
const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest; const purchaseRequest = JSON.parse(String(req.body)) as IPurchaseRequest;
@ -12,5 +11,4 @@ export const purchaseController: RequestHandler = async (req, res) => {
const response = await handlePurchase(purchaseRequest, inventory); const response = await handlePurchase(purchaseRequest, inventory);
await inventory.save(); await inventory.save();
res.json(response); res.json(response);
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };

View File

@ -1,25 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
// Basic shim handling action=sync to login on U21
export const questControlController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const quests: IQuestState[] = [];
for (const quest of inventory.QuestKeys) {
quests.push({
quest: quest.ItemType,
state: 3 // COMPLETE
});
}
res.json({
QuestState: quests
});
};
interface IQuestState {
quest: string;
state: number;
task?: string;
}

View File

@ -1,99 +0,0 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const removeFriendGetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
if (req.query.all) {
const [internalFriendships, externalFriendships] = await Promise.all([
Friendship.find({ owner: accountId }, "friend"),
Friendship.find({ friend: accountId }, "owner")
]);
const promises: Promise<void>[] = [];
const friends: IOid[] = [];
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
promises.push(Friendship.deleteOne({ _id: externalFriendship._id }) as unknown as Promise<void>);
friends.push(toOid(externalFriendship.owner));
}
}
await Promise.all(promises);
res.json({
Friends: friends
} satisfies IRemoveFriendsResponse);
} else {
const friendId = req.query.friendId as string;
await Promise.all([
Friendship.deleteOne({ owner: accountId, friend: friendId }),
Friendship.deleteOne({ owner: friendId, friend: accountId })
]);
res.json({
Friends: [{ $oid: friendId }]
} satisfies IRemoveFriendsResponse);
}
};
export const removeFriendPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IBatchRemoveFriendsRequest>(String(req.body));
const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend));
// TOVERIFY: Should pending friendships also be kept?
// Keep friends that have been online within threshold
await parallelForeach([...friends], async friend => {
const account = (await Account.findById(friend, "LastLogin"))!;
const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000;
if (daysLoggedOut < data.DaysLoggedOut) {
friends.delete(friend);
}
});
if (data.SkipClanmates) {
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
await parallelForeach([...friends], async friend => {
const friendInventory = await getInventory(friend.toString(), "GuildId");
if (friendInventory.GuildId?.equals(inventory.GuildId)) {
friends.delete(friend);
}
});
}
}
// Remove all remaining friends that aren't in SkipFriendIds & give response.
const promises = [];
const response: IOid[] = [];
for (const friend of friends) {
if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) {
promises.push(Friendship.deleteOne({ owner: accountId, friend: friend }));
promises.push(Friendship.deleteOne({ owner: friend, friend: accountId }));
response.push(toOid(friend));
}
}
await Promise.all(promises);
res.json({
Friends: response
} satisfies IRemoveFriendsResponse);
};
// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075.
const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => {
return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8);
};
interface IBatchRemoveFriendsRequest {
DaysLoggedOut: number;
SkipClanmates: boolean;
SkipFriendIds: string[];
}
interface IRemoveFriendsResponse {
Friends: IOid[];
}

View File

@ -1,32 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/webService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
export const renamePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree");
const data = getJSONfromString<IRenamePetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.Name = data.name;
const inventoryChanges: IInventoryChanges = {};
if (!("webui" in req.query)) {
updateCurrency(inventory, 15, true, inventoryChanges);
}
await inventory.save();
res.json({
...data,
inventoryChanges: inventoryChanges
});
sendWsBroadcastTo(accountId, { update_inventory: true });
};
interface IRenamePetRequest {
petId: string;
name: string;
}

View File

@ -1,8 +1,10 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => { export const saveDialogueController: RequestHandler = async (req, res) => {
@ -24,34 +26,34 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
inventory.DialogueHistory.Dialogues ??= []; inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName); const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank; dialogue.Rank = request.Rank;
dialogue.Chemistry += request.Chemistry; dialogue.Chemistry = request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
if (bool == "LizzieShawzin") {
await addEmailItem(
inventory,
"/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
inventoryChanges
);
}
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
if (index != -1) {
dialogue.Booleans.splice(index, 1);
}
}
for (const info of request.OtherDialogueInfos) {
const otherDialogue = getDialogue(inventory, info.Dialogue);
if (info.Tag != "") {
otherDialogue.QueuedDialogues.push(info.Tag);
}
otherDialogue.Chemistry += info.Value; // unsure
}
if (request.Data) { if (request.Data) {
dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
if (bool == "LizzieShawzin") {
await addEmailItem(
inventory,
"/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
inventoryChanges
);
}
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
if (index != -1) {
dialogue.Booleans.splice(index, 1);
}
}
dialogue.Completed.push(request.Data); dialogue.Completed.push(request.Data);
dialogue.AvailableDate = new Date(tomorrowAt0Utc); dialogue.AvailableDate = new Date(tomorrowAt0Utc);
for (const info of request.OtherDialogueInfos) {
const otherDialogue = getDialogue(inventory, info.Dialogue);
if (info.Tag != "") {
otherDialogue.QueuedDialogues.push(info.Tag);
}
otherDialogue.Chemistry += info.Value; // unsure
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
@ -72,7 +74,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
}); });
} else { } else {
res.end(); logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
} }
} }
}; };
@ -106,3 +108,26 @@ interface IOtherDialogueInfo {
Tag: string; Tag: string;
Value: number; Value: number;
} }
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};

View File

@ -2,12 +2,11 @@ import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
export const saveLoadoutController: RequestHandler = async (req, res) => { export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = getJSONfromString<ISaveLoadoutRequest>(String(req.body)); const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -8,14 +8,12 @@ import {
addConsumables, addConsumables,
freeUpSlot, freeUpSlot,
combineInventoryChanges, combineInventoryChanges,
addCrewShipRawSalvage, addCrewShipRawSalvage
addFusionPoints
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { sendWsBroadcastTo } from "@/src/services/webService";
export const sellController: RequestHandler = async (req, res) => { export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest; const payload = JSON.parse(String(req.body)) as ISellRequest;
@ -46,10 +44,7 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) { if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS); requiredFields.add(InventorySlot.SPACEWEAPONS);
} }
if (payload.Items.MechSuits) { if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
requiredFields.add(InventorySlot.MECHSUITS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) {
requiredFields.add(InventorySlot.SENTINELS); requiredFields.add(InventorySlot.SENTINELS);
} }
if (payload.Items.OperatorAmps) { if (payload.Items.OperatorAmps) {
@ -74,7 +69,7 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.SellCurrency == "SC_RegularCredits") { if (payload.SellCurrency == "SC_RegularCredits") {
inventory.RegularCredits += payload.SellPrice; inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") { } else if (payload.SellCurrency == "SC_FusionPoints") {
addFusionPoints(inventory, payload.SellPrice); inventory.FusionPoints += payload.SellPrice;
} else if (payload.SellCurrency == "SC_PrimeBucks") { } else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -140,12 +135,6 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
}); });
} }
if (payload.Items.MechSuits) {
payload.Items.MechSuits.forEach(sellItem => {
inventory.MechSuits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.MECHSUITS);
});
}
if (payload.Items.Sentinels) { if (payload.Items.Sentinels) {
payload.Items.Sentinels.forEach(sellItem => { payload.Items.Sentinels.forEach(sellItem => {
inventory.Sentinels.pull({ _id: sellItem.String }); inventory.Sentinels.pull({ _id: sellItem.String });
@ -158,12 +147,6 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SENTINELS); freeUpSlot(inventory, InventorySlot.SENTINELS);
}); });
} }
if (payload.Items.MoaPets) {
payload.Items.MoaPets.forEach(sellItem => {
inventory.MoaPets.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.OperatorAmps) { if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => { payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String }); inventory.OperatorAmps.pull({ _id: sellItem.String });
@ -280,7 +263,6 @@ export const sellController: RequestHandler = async (req, res) => {
res.json({ res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };
interface ISellRequest { interface ISellRequest {
@ -296,10 +278,8 @@ interface ISellRequest {
SpaceSuits?: ISellItem[]; SpaceSuits?: ISellItem[];
SpaceGuns?: ISellItem[]; SpaceGuns?: ISellItem[];
SpaceMelee?: ISellItem[]; SpaceMelee?: ISellItem[];
MechSuits?: ISellItem[];
Sentinels?: ISellItem[]; Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[]; SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[];
OperatorAmps?: ISellItem[]; OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[]; Hoverboards?: ISellItem[];
Drones?: ISellItem[]; Drones?: ISellItem[];

View File

@ -13,7 +13,7 @@ export const setDojoComponentSettingsController: RequestHandler = async (req, re
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
const component = guild.DojoComponents.id(req.query.componentId as string)!; const component = guild.DojoComponents.id(req.query.componentId)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body)); const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings; component.Settings = data.Settings;
await guild.save(); await guild.save();

View File

@ -1,30 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const setFriendNoteController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<ISetFriendNoteRequest>(String(req.body));
const friendship = await Friendship.findOne({ owner: accountId, friend: payload.FriendId }, "Note Favorite");
if (friendship) {
if ("Note" in payload) {
friendship.Note = payload.Note;
} else {
friendship.Favorite = payload.Favorite;
}
await friendship.save();
}
res.json({
Id: payload.FriendId,
SetNote: "Note" in payload,
Note: friendship?.Note,
Favorite: friendship?.Favorite
});
};
interface ISetFriendNoteRequest {
FriendId: string;
Note?: string;
Favorite?: boolean;
}

View File

@ -1,4 +1,3 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel"; import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermissionEx } from "@/src/services/guildService"; import { hasGuildPermissionEx } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
@ -56,9 +55,5 @@ export const setGuildMotdController: RequestHandler = async (req, res) => {
await guild.save(); await guild.save();
} }
if (!account.BuildLabel || version_compare(account.BuildLabel, "2020.03.24.20.24") > 0) { res.json({ IsLongMOTD, MOTD });
res.json({ IsLongMOTD, MOTD });
} else {
res.send(MOTD).end();
}
}; };

View File

@ -20,7 +20,7 @@ export const setShipFavouriteLoadoutController: RequestHandler = async (req, res
throw new Error(`unexpected BootLocation: ${body.BootLocation}`); throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
} }
await personalRooms.save(); await personalRooms.save();
res.json(body); res.json({});
}; };
interface ISetShipFavouriteLoadoutRequest { interface ISetShipFavouriteLoadoutRequest {

View File

@ -1,22 +0,0 @@
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { RequestHandler } from "express";
export const setSuitInfectionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Suits");
const payload = getJSONfromString<ISetSuitInfectionRequest>(String(req.body));
for (const clientSuit of payload.Suits) {
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
dbSuit.InfestationDate = fromMongoDate(clientSuit.InfestationDate!);
}
await inventory.save();
res.end();
};
interface ISetSuitInfectionRequest {
Suits: IEquipmentClient[];
}

View File

@ -11,7 +11,7 @@ import {
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
interface IStartDojoRecipeRequest { interface IStartDojoRecipeRequest {
@ -20,10 +20,10 @@ interface IStartDojoRecipeRequest {
} }
export const startDojoRecipeController: RequestHandler = async (req, res) => { export const startDojoRecipeController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys"); const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory); const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, account._id, GuildPermission.Architect))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
@ -64,5 +64,5 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
setDojoRoomLogFunded(guild, component); setDojoRoomLogFunded(guild, component);
} }
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0, undefined, account.BuildLabel)); res.json(await getDojoClient(guild, 0));
}; };

View File

@ -3,14 +3,12 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { ExportWeapons } from "warframe-public-export-plus"; import { ExportWeapons } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
interface IStartRecipeRequest { interface IStartRecipeRequest {
RecipeName: string; RecipeName: string;
@ -43,36 +41,25 @@ export const startRecipeController: RequestHandler = async (req, res) => {
]; ];
for (let i = 0; i != recipe.ingredients.length; ++i) { for (let i = 0; i != recipe.ingredients.length; ++i) {
if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { if (startRecipeRequest.Ids[i]) {
if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
if (index != -1) { throw new Error(`unexpected equipment ingredient type: ${category}`);
inventory.KubrowPetEggs!.splice(index, 1);
}
} else {
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
throw new Error(`unexpected equipment ingredient type: ${category}`);
}
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
freeUpSlot(inventory, InventorySlot.WEAPONS);
} }
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
freeUpSlot(inventory, InventorySlot.WEAPONS);
} else { } else {
await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
} }
} }
let inventoryChanges: IInventoryChanges | undefined; if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
} else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const spectreLoadout: ISpectreLoadout = { const spectreLoadout: ISpectreLoadout = {
ItemType: recipe.resultType, ItemType: recipe.resultType,
Suits: "", Suits: "",
@ -129,5 +116,5 @@ export const startRecipeController: RequestHandler = async (req, res) => {
await inventory.save(); await inventory.save();
res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges }); res.json({ RecipeId: toOid(pr._id) });
}; };

View File

@ -1,12 +1,11 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { toStoreItem } from "@/src/services/itemDataService"; import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
import { logger } from "@/src/utils/logger";
export const syndicateSacrificeController: RequestHandler = async (request, response) => { export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
@ -18,83 +17,70 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1]; syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
} }
const oldLevel = syndicate.Title ?? 0; const level = data.SacrificeLevel - (syndicate.Title ?? 0);
const levelIncrease = data.SacrificeLevel - oldLevel;
if (levelIncrease < 0) {
throw new Error(`syndicate sacrifice can not decrease level`);
}
if (levelIncrease > 1 && !data.AllowMultiple) {
throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`);
}
const res: ISyndicateSacrificeResponse = { const res: ISyndicateSacrificeResponse = {
AffiliationTag: data.AffiliationTag, AffiliationTag: data.AffiliationTag,
InventoryChanges: {}, InventoryChanges: {},
Level: data.SacrificeLevel, Level: data.SacrificeLevel,
LevelIncrease: data.SacrificeLevel < 0 ? 1 : levelIncrease, LevelIncrease: level <= 0 ? 1 : level,
NewEpisodeReward: false NewEpisodeReward: false
}; };
// Process sacrifices and rewards for every level we're reaching
const manifest = ExportSyndicates[data.AffiliationTag]; const manifest = ExportSyndicates[data.AffiliationTag];
for (let level = oldLevel + Math.min(levelIncrease, 1); level <= data.SacrificeLevel; ++level) { let sacrifice: ISyndicateSacrifice | undefined;
let sacrifice: ISyndicateSacrifice | undefined; let reward: string | undefined;
if (level == 0) { if (data.SacrificeLevel == 0) {
sacrifice = manifest.initiationSacrifice; sacrifice = manifest.initiationSacrifice;
if (manifest.initiationReward) { reward = manifest.initiationReward;
combineInventoryChanges( syndicate.Initiated = true;
res.InventoryChanges, } else {
(await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
); }
}
syndicate.Initiated = true;
} else {
sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice;
}
if (sacrifice) { if (sacrifice) {
updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges); res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
for (const item of sacrifice.items) { const miscItemChanges = sacrifice.items.map(x => ({
addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges); ItemType: x.ItemType,
} ItemCount: x.ItemCount * -1
} }));
addMiscItems(inventory, miscItemChanges);
res.InventoryChanges.MiscItems = miscItemChanges;
}
// Quacks like a nightwave syndicate? syndicate.Title ??= 0;
if (manifest.dailyChallenges) { syndicate.Title += 1;
const title = manifest.titles!.find(x => x.level == level);
if (title) { if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
res.NewEpisodeReward = true; syndicate.FreeFavorsEarned ??= [];
let rewardType: string; if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
let rewardCount: number; syndicate.FreeFavorsEarned.push(syndicate.Title);
if (title.storeItemReward) { }
rewardType = title.storeItemReward; }
rewardCount = 1;
} else { if (reward) {
rewardType = toStoreItem(title.reward!.ItemType); combineInventoryChanges(
rewardCount = title.reward!.ItemCount; res.InventoryChanges,
} (await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) );
.InventoryChanges; }
if (Object.keys(rewardInventoryChanges).length == 0) {
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); if (data.AffiliationTag == ExportNightwave.affiliationTag) {
const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; const index = syndicate.Title - 1;
addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges); if (index < ExportNightwave.rewards.length) {
} res.NewEpisodeReward = true;
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); const reward = ExportNightwave.rewards[index];
} let rewardType = reward.uniqueName;
} else { if (!isStoreItem(rewardType)) {
if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) { rewardType = toStoreItem(rewardType);
syndicate.FreeFavorsEarned ??= []; }
if (!syndicate.FreeFavorsEarned.includes(level)) { combineInventoryChanges(
syndicate.FreeFavorsEarned.push(level); res.InventoryChanges,
} (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges
} );
} }
} }
// Commit
syndicate.Title = data.SacrificeLevel < 0 ? data.SacrificeLevel + 1 : data.SacrificeLevel;
await inventory.save(); await inventory.save();
response.json(res); response.json(res);

View File

@ -5,7 +5,7 @@ import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTy
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IAffiliationMods, IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const syndicateStandingBonusController: RequestHandler = async (req, res) => { export const syndicateStandingBonusController: RequestHandler = async (req, res) => {
@ -54,14 +54,13 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
} }
const affiliationMods: IAffiliationMods[] = []; const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, affiliationMods, true);
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods AffiliationMods: [affiliationMod]
}); });
}; };

View File

@ -35,17 +35,6 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
inventory.PlayerLevel += 1; inventory.PlayerLevel += 1;
inventory.TradesRemaining += 1; inventory.TradesRemaining += 1;
if (inventory.PlayerLevel == 2) {
await createMessage(accountId, [
{
sndr: "/Lotus/Language/Game/Maroo",
msg: "/Lotus/Language/Clan/MarooClanSearchDesc",
sub: "/Lotus/Language/Clan/MarooClanSearchTitle",
icon: "/Lotus/Interface/Icons/Npcs/Maroo.png"
}
]);
}
await createMessage(accountId, [ await createMessage(accountId, [
{ {
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",

View File

@ -1,27 +0,0 @@
import { fromMongoDate, fromOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { RequestHandler } from "express";
export const umbraController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Suits MiscItems");
const payload = getJSONfromString<IUmbraRequest>(String(req.body));
for (const clientSuit of payload.Suits) {
const dbSuit = inventory.Suits.id(fromOid(clientSuit.ItemId))!;
if (clientSuit.UmbraDate) {
addMiscItem(inventory, "/Lotus/Types/Items/MiscItems/UmbraEchoes", -1);
dbSuit.UmbraDate = fromMongoDate(clientSuit.UmbraDate);
} else {
dbSuit.UmbraDate = undefined;
}
}
await inventory.save();
res.end();
};
interface IUmbraRequest {
Suits: IEquipmentClient[];
}

View File

@ -1,54 +1,28 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { IAffiliationMods } from "@/src/types/purchaseTypes"; import { IAffiliationMods } from "@/src/types/purchaseTypes";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { logger } from "@/src/utils/logger";
export const updateChallengeProgressController: RequestHandler = async (req, res) => { export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body)); const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory( const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
account._id.toString(),
"ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress"
);
let affiliationMods: IAffiliationMods[] = []; let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) { if (challenges.ChallengeProgress) {
affiliationMods = addChallenges( affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
account,
inventory,
challenges.ChallengeProgress,
challenges.SeasonChallengeCompletions
);
} }
for (const [key, value] of getEntriesUnsafe(challenges)) { if (challenges.SeasonChallengeHistory) {
switch (key) { challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
case "ChallengesFixVersion": const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
inventory.ChallengesFixVersion = value; if (itemIndex !== -1) {
break; inventory.SeasonChallengeHistory[itemIndex].id = id;
} else {
case "SeasonChallengeHistory": inventory.SeasonChallengeHistory.push({ challenge, id });
value!.forEach(({ challenge, id }) => { }
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge); });
if (itemIndex !== -1) {
inventory.SeasonChallengeHistory[itemIndex].id = id;
} else {
inventory.SeasonChallengeHistory.push({ challenge, id });
}
});
break;
case "ChallengeProgress":
case "SeasonChallengeCompletions":
case "ChallengePTS":
case "crossPlaySetting":
break;
default:
logger.warn(`unknown challenge progress entry`, { key, value });
}
} }
await inventory.save(); await inventory.save();
@ -58,10 +32,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
}; };
interface IUpdateChallengeProgressRequest { interface IUpdateChallengeProgressRequest {
ChallengePTS?: number;
ChallengesFixVersion?: number;
ChallengeProgress?: IChallengeProgress[]; ChallengeProgress?: IChallengeProgress[];
SeasonChallengeHistory?: ISeasonChallenge[]; SeasonChallengeHistory?: ISeasonChallenge[];
SeasonChallengeCompletions?: ISeasonChallenge[]; SeasonChallengeCompletions?: ISeasonChallenge[];
crossPlaySetting?: string;
} }

View File

@ -1,16 +1,12 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
export const addCurrencyController: RequestHandler = async (req, res) => { export const addCurrencyController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = req.body as IAddCurrencyRequest; const request = req.body as IAddCurrencyRequest;
const inventory = await getInventory(accountId, request.currency); inventory[request.currency] += request.delta;
if (request.currency == "FusionPoints") {
addFusionPoints(inventory, request.delta);
} else {
inventory[request.currency] += request.delta;
}
await inventory.save(); await inventory.save();
res.end(); res.end();
}; };

View File

@ -7,7 +7,7 @@ export const addItemsController: RequestHandler = async (req, res) => {
const requests = req.body as IAddItemRequest[]; const requests = req.body as IAddItemRequest[];
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
for (const request of requests) { for (const request of requests) {
await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, undefined, true); await addItem(inventory, request.ItemType, request.ItemCount, true);
} }
await inventory.save(); await inventory.save();
res.end(); res.end();

View File

@ -1,24 +0,0 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addRecipes } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { ExportRecipes } from "warframe-public-export-plus";
export const addMissingHelminthBlueprintsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Recipes");
const allHelminthRecipes = Object.keys(ExportRecipes).filter(
key => ExportRecipes[key].secretIngredientAction === "SIA_WARFRAME_ABILITY"
);
const inventoryHelminthRecipes = inventory.Recipes.filter(recipe =>
recipe.ItemType.startsWith("/Lotus/Types/Recipes/AbilityOverrides/")
).map(recipe => recipe.ItemType);
const missingHelminthRecipes = allHelminthRecipes
.filter(key => !inventoryHelminthRecipes.includes(key))
.map(ItemType => ({ ItemType, ItemCount: 1 }));
addRecipes(inventory, missingHelminthRecipes);
await inventory.save();
res.end();
};

View File

@ -1,44 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus";
export const addMissingMaxRankModsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Upgrades");
const maxOwnedRanks: Record<string, number> = {};
for (const upgrade of inventory.Upgrades) {
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint ?? "{}") as { lvl?: number };
if (fingerprint.lvl) {
maxOwnedRanks[upgrade.ItemType] ??= 0;
if (fingerprint.lvl > maxOwnedRanks[upgrade.ItemType]) {
maxOwnedRanks[upgrade.ItemType] = fingerprint.lvl;
}
}
}
for (const [uniqueName, data] of Object.entries(ExportUpgrades)) {
if (data.fusionLimit != 0 && data.type != "PARAZON" && maxOwnedRanks[uniqueName] != data.fusionLimit) {
inventory.Upgrades.push({
ItemType: uniqueName,
UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit })
});
}
}
for (const [uniqueName, data] of Object.entries(ExportArcanes)) {
if (
data.name != "/Lotus/Language/Items/GenericCosmeticEnhancerName" &&
maxOwnedRanks[uniqueName] != data.fusionLimit
) {
inventory.Upgrades.push({
ItemType: uniqueName,
UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit })
});
}
}
await inventory.save();
res.end();
};

View File

@ -1,6 +1,5 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { addGearExpByCategory, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -12,7 +11,7 @@ export const addXpController: RequestHandler = async (req, res) => {
const request = req.body as IAddXpRequest; const request = req.body as IAddXpRequest;
for (const [category, gear] of Object.entries(request)) { for (const [category, gear] of Object.entries(request)) {
for (const clientItem of gear) { for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid); const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid);
if (dbItem) { if (dbItem) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) { if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) { if ((dbItem.Polarized ?? 0) < 5) {
@ -21,7 +20,7 @@ export const addXpController: RequestHandler = async (req, res) => {
} }
} }
} }
applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey); addGearExpByCategory(inventory, gear, category as TEquipmentKey);
} }
await inventory.save(); await inventory.save();
res.end(); res.end();

View File

@ -1,40 +0,0 @@
import { addString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFixedLevelRewards } from "@/src/services/missionInventoryUpdateService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IMissionReward } from "@/src/types/missionTypes";
import { RequestHandler } from "express";
import { ExportRegions } from "warframe-public-export-plus";
export const completeAllMissionsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const MissionRewards: IMissionReward[] = [];
for (const [tag, node] of Object.entries(ExportRegions)) {
let mission = inventory.Missions.find(x => x.Tag == tag);
if (!mission) {
mission =
inventory.Missions[
inventory.Missions.push({
Completes: 0,
Tier: 0,
Tag: tag
}) - 1
];
}
if (mission.Completes == 0) {
mission.Completes++;
if (node.missionReward) {
addFixedLevelRewards(node.missionReward, inventory, MissionRewards);
}
}
mission.Tier = 1;
}
for (const reward of MissionRewards) {
await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount, undefined, true);
}
addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked");
await inventory.save();
res.end();
};

View File

@ -10,7 +10,6 @@ import { Stats } from "@/src/models/statsModel";
import { GuildMember } from "@/src/models/guildModel"; import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel"; import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService"; import { deleteGuild } from "@/src/services/guildService";
import { Friendship } from "@/src/models/friendModel";
export const deleteAccountController: RequestHandler = async (req, res) => { export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -23,8 +22,6 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
await Promise.all([ await Promise.all([
Account.deleteOne({ _id: accountId }), Account.deleteOne({ _id: accountId }),
Friendship.deleteMany({ owner: accountId }),
Friendship.deleteMany({ friend: accountId }),
GuildMember.deleteMany({ accountId: accountId }), GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }), Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }), Ignore.deleteMany({ ignoree: accountId }),

View File

@ -1,18 +1,15 @@
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getAccountInfoController: RequestHandler = async (req, res) => { export const getAccountInfoController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "QuestKeys");
const info: IAccountInfo = { const info: IAccountInfo = {
DisplayName: account.DisplayName, DisplayName: account.DisplayName
IsAdministrator: isAdministrator(account),
CompletedVorsPrize: !!inventory.QuestKeys.find(
x => x.ItemType == "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"
)?.Completed
}; };
if (isAdministrator(account)) {
info.IsAdministrator = true;
}
const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank");
if (guildMember) { if (guildMember) {
const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!; const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!;
@ -34,8 +31,7 @@ export const getAccountInfoController: RequestHandler = async (req, res) => {
interface IAccountInfo { interface IAccountInfo {
DisplayName: string; DisplayName: string;
IsAdministrator: boolean; IsAdministrator?: boolean;
CompletedVorsPrize: boolean;
GuildId?: string; GuildId?: string;
GuildPermissions?: number; GuildPermissions?: number;
GuildRank?: number; GuildRank?: number;

View File

@ -3,7 +3,6 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
import { import {
ExportArcanes, ExportArcanes,
ExportAvionics, ExportAvionics,
ExportBoosters,
ExportCustoms, ExportCustoms,
ExportDrones, ExportDrones,
ExportGear, ExportGear,
@ -20,12 +19,11 @@ import {
ExportWeapons, ExportWeapons,
TRelicQuality TRelicQuality
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
interface ListedItem { interface ListedItem {
uniqueName: string; uniqueName: string;
name: string; name: string;
subtype?: string;
fusionLimit?: number; fusionLimit?: number;
exalted?: string[]; exalted?: string[];
badReason?: "starter" | "frivolous" | "notraw"; badReason?: "starter" | "frivolous" | "notraw";
@ -34,29 +32,6 @@ interface ListedItem {
parazon?: boolean; parazon?: boolean;
} }
interface ItemLists {
uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[];
LongGuns: ListedItem[];
Melee: ListedItem[];
ModularParts: ListedItem[];
Pistols: ListedItem[];
Sentinels: ListedItem[];
SentinelWeapons: ListedItem[];
SpaceGuns: ListedItem[];
SpaceMelee: ListedItem[];
SpaceSuits: ListedItem[];
MechSuits: ListedItem[];
miscitems: ListedItem[];
Syndicates: ListedItem[];
OperatorAmps: ListedItem[];
QuestKeys: ListedItem[];
KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[];
mods: ListedItem[];
Boosters: ListedItem[];
}
const relicQualitySuffixes: Record<TRelicQuality, string> = { const relicQualitySuffixes: Record<TRelicQuality, string> = {
VPQ_BRONZE: "", VPQ_BRONZE: "",
VPQ_SILVER: " [Flawless]", VPQ_SILVER: " [Flawless]",
@ -66,28 +41,23 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
const getItemListsController: RequestHandler = (req, response) => { const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = { const res: Record<string, ListedItem[]> = {};
uniqueLevelCaps: ExportMisc.uniqueLevelCaps, res.Suits = [];
Suits: [], res.LongGuns = [];
LongGuns: [], res.Melee = [];
Melee: [], res.ModularParts = [];
ModularParts: [], res.Pistols = [];
Pistols: [], res.Sentinels = [];
Sentinels: [], res.SentinelWeapons = [];
SentinelWeapons: [], res.SpaceGuns = [];
SpaceGuns: [], res.SpaceMelee = [];
SpaceMelee: [], res.SpaceSuits = [];
SpaceSuits: [], res.MechSuits = [];
MechSuits: [], res.miscitems = [];
miscitems: [], res.Syndicates = [];
Syndicates: [], res.OperatorAmps = [];
OperatorAmps: [], res.QuestKeys = [];
QuestKeys: [], res.KubrowPets = [];
KubrowPets: [],
EvolutionProgress: [],
mods: [],
Boosters: []
};
for (const [uniqueName, item] of Object.entries(ExportWarframes)) { for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({ res[item.productCategory].push({
uniqueName, uniqueName,
@ -96,7 +66,7 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
for (const [uniqueName, item] of Object.entries(ExportSentinels)) { for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") { if (item.productCategory != "SpecialItems") {
res[item.productCategory].push({ res[item.productCategory].push({
uniqueName, uniqueName,
name: getString(item.name, lang), name: getString(item.name, lang),
@ -145,28 +115,12 @@ const getItemListsController: RequestHandler = (req, response) => {
let name = getString(item.name, lang); let name = getString(item.name, lang);
if ("dissectionParts" in item) { if ("dissectionParts" in item) {
name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name); name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name);
if (item.syndicateTag == "CetusSyndicate") { if (uniqueName.indexOf("Large") != -1) {
if (uniqueName.indexOf("Large") != -1) { name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang));
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang)); } else if (uniqueName.indexOf("Medium") != -1) {
} else if (uniqueName.indexOf("Medium") != -1) { name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
} else {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
}
} else { } else {
if (uniqueName.indexOf("Large") != -1) { name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryElderAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryMatureAbbrev", lang));
} else {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryYoungAbbrev", lang));
}
} }
} }
if ( if (
@ -176,8 +130,7 @@ const getItemListsController: RequestHandler = (req, response) => {
) { ) {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: name, name: name
subtype: "Resource"
}); });
} }
} }
@ -195,8 +148,7 @@ const getItemListsController: RequestHandler = (req, response) => {
for (const [uniqueName, item] of Object.entries(ExportGear)) { for (const [uniqueName, item] of Object.entries(ExportGear)) {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: getString(item.name, lang), name: getString(item.name, lang)
subtype: "Gear"
}); });
} }
const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang); const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
@ -232,6 +184,7 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
res.mods = [];
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
const mod: ListedItem = { const mod: ListedItem = {
uniqueName, uniqueName,
@ -289,21 +242,12 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
} }
for (const uniqueName of allIncarnons) {
res.EvolutionProgress.push({
uniqueName,
name: getString(getItemName(uniqueName) || "", lang)
});
}
for (const item of Object.values(ExportBoosters)) { response.json({
res.Boosters.push({ archonCrystalUpgrades,
uniqueName: item.typeName, uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
name: getString(item.name, lang) ...res
}); });
}
response.json(res);
}; };
export { getItemListsController }; export { getItemListsController };

View File

@ -128,7 +128,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
await completeQuest(inventory, questKey.ItemType); await completeQuest(inventory, questKey.ItemType);
} else { } else {
const progress = { const progress = {
c: 0, c: questManifest.chainStages![currentStage].key ? -1 : 0,
i: false, i: false,
m: false, m: false,
b: [] b: []

View File

@ -12,7 +12,6 @@ export const popArchonCrystalUpgradeController: RequestHandler = async (req, res
); );
await inventory.save(); await inventory.save();
res.end(); res.end();
return;
} }
res.status(400).end(); res.status(400).end();
}; };

View File

@ -15,7 +15,6 @@ export const pushArchonCrystalUpgradeController: RequestHandler = async (req, re
} }
await inventory.save(); await inventory.save();
res.end(); res.end();
return;
} }
} }
res.status(400).end(); res.status(400).end();

View File

@ -1,45 +0,0 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
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 { ItemType: string; ExpiryDate: number }[];
const inventory = await getInventory(accountId, "Boosters");
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.floor(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 {
const boosterItem = boosters.find(item => item.ItemType === ItemType);
if (boosterItem) {
boosterItem.ExpiryDate = ExpiryDate;
} else {
boosters.push({ ItemType, ExpiryDate });
}
}
}
await inventory.save();
res.end();
};

View File

@ -1,33 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const setEvolutionProgressController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = req.body as ISetEvolutionProgressRequest;
inventory.EvolutionProgress ??= [];
payload.forEach(element => {
const entry = inventory.EvolutionProgress!.find(entry => entry.ItemType === element.ItemType);
if (entry) {
entry.Progress = 0;
entry.Rank = element.Rank;
} else {
inventory.EvolutionProgress!.push({
Progress: 0,
Rank: element.Rank,
ItemType: element.ItemType
});
}
});
await inventory.save();
res.end();
};
type ISetEvolutionProgressRequest = {
ItemType: string;
Rank: number;
}[];

View File

@ -1,21 +1,15 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { saveConfig } from "@/src/services/configWatcherService"; import { updateConfig } from "@/src/services/configWatcherService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { config, IConfig } from "@/src/services/configService";
export const updateConfigDataController: RequestHandler = async (req, res) => { const updateConfigDataController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
if (isAdministrator(account)) { if (isAdministrator(account)) {
const data = req.body as IUpdateConfigDataRequest; await updateConfig(String(req.body));
config[data.key] = data.value;
await saveConfig();
res.end(); res.end();
} else { } else {
res.status(401).end(); res.status(401).end();
} }
}; };
interface IUpdateConfigDataRequest { export { updateConfigDataController };
key: keyof IConfig;
value: never;
}

View File

@ -1,10 +0,0 @@
import { args } from "@/src/helpers/commandLineArguments";
import { sendWsBroadcast } from "@/src/services/webService";
import { RequestHandler } from "express";
export const webuiFileChangeDetectedController: RequestHandler = (req, res) => {
if (args.dev && args.secret && req.query.secret == args.secret) {
sendWsBroadcast({ reload: true });
}
res.end();
};

View File

@ -18,76 +18,64 @@ import {
ITypeXPItem ITypeXPItem
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { catBreadHash, getJSONfromString } from "@/src/helpers/stringHelpers"; import { catBreadHash } from "@/src/helpers/stringHelpers";
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
import { IStatsClient } from "@/src/types/statTypes"; import { IStatsClient } from "@/src/types/statTypes";
import { toStoreItem } from "@/src/services/itemDataService"; import { toStoreItem } from "@/src/services/itemDataService";
import { FlattenMaps } from "mongoose";
const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IProfileViewingData | undefined> => { export const getProfileViewingDataController: RequestHandler = async (req, res) => {
const account = await Account.findById(playerId, "DisplayName");
if (!account) {
return;
}
const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!;
const result: IPlayerProfileViewingDataResult = {
AccountId: toOid(account._id),
DisplayName: account.DisplayName,
PlayerLevel: inventory.PlayerLevel,
LoadOutInventory: {
WeaponSkins: [],
XPInfo: inventory.XPInfo
},
PlayerSkills: inventory.PlayerSkills,
ChallengeProgress: inventory.ChallengeProgress,
DeathMarks: inventory.DeathMarks,
Harvestable: inventory.Harvestable,
DeathSquadable: inventory.DeathSquadable,
Created: toMongoDate(inventory.Created),
MigratedToConsole: false,
Missions: inventory.Missions,
Affiliations: inventory.Affiliations,
DailyFocus: inventory.DailyFocus,
Wishlist: inventory.Wishlist,
Alignment: inventory.Alignment
};
await populateLoadout(inventory, result);
if (inventory.GuildId) {
const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!;
populateGuild(guild, result);
}
for (const key of allDailyAffiliationKeys) {
result[key] = inventory[key];
}
const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON<Partial<TStatsDatabaseDocument>>();
delete stats._id;
delete stats.__v;
delete stats.accountOwnerId;
return {
Results: [result],
TechProjects: [],
XpComponents: [],
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
Stats: stats
};
};
export const getProfileViewingDataGetController: RequestHandler = async (req, res) => {
if (req.query.playerId) { if (req.query.playerId) {
const data = await getProfileViewingDataByPlayerIdImpl(req.query.playerId as string); const account = await Account.findById(req.query.playerId as string, "DisplayName");
if (data) { if (!account) {
res.json(data);
} else {
res.status(409).send("Could not find requested account"); res.status(409).send("Could not find requested account");
return;
} }
const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!;
const result: IPlayerProfileViewingDataResult = {
AccountId: toOid(account._id),
DisplayName: account.DisplayName,
PlayerLevel: inventory.PlayerLevel,
LoadOutInventory: {
WeaponSkins: [],
XPInfo: inventory.XPInfo
},
PlayerSkills: inventory.PlayerSkills,
ChallengeProgress: inventory.ChallengeProgress,
DeathMarks: inventory.DeathMarks,
Harvestable: inventory.Harvestable,
DeathSquadable: inventory.DeathSquadable,
Created: toMongoDate(inventory.Created),
MigratedToConsole: false,
Missions: inventory.Missions,
Affiliations: inventory.Affiliations,
DailyFocus: inventory.DailyFocus,
Wishlist: inventory.Wishlist,
Alignment: inventory.Alignment
};
await populateLoadout(inventory, result);
if (inventory.GuildId) {
const guild = (await Guild.findById(inventory.GuildId, "Name Tier XP Class Emblem"))!;
populateGuild(guild, result);
}
for (const key of allDailyAffiliationKeys) {
result[key] = inventory[key];
}
const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON<Partial<TStatsDatabaseDocument>>();
delete stats._id;
delete stats.__v;
delete stats.accountOwnerId;
res.json({
Results: [result],
TechProjects: [],
XpComponents: [],
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
Stats: stats
});
} else if (req.query.guildId) { } else if (req.query.guildId) {
const guild = await Guild.findById( const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP");
req.query.guildId as string,
"Name Tier XP Class Emblem TechProjects ClaimedXP"
);
if (!guild) { if (!guild) {
res.status(409).send("Could not find guild"); res.status(409).send("Could not find guild");
return; return;
@ -182,28 +170,6 @@ export const getProfileViewingDataGetController: RequestHandler = async (req, re
} }
}; };
// For old versions, this was an authenticated POST request.
interface IGetProfileViewingDataRequest {
AccountId: string;
}
export const getProfileViewingDataPostController: RequestHandler = async (req, res) => {
const payload = getJSONfromString<IGetProfileViewingDataRequest>(String(req.body));
const data = await getProfileViewingDataByPlayerIdImpl(payload.AccountId);
if (data) {
res.json(data);
} else {
res.status(409).send("Could not find requested account");
}
};
interface IProfileViewingData {
Results: IPlayerProfileViewingDataResult[];
TechProjects: [];
XpComponents: [];
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
Stats: FlattenMaps<Partial<TStatsDatabaseDocument>>;
}
interface IPlayerProfileViewingDataResult extends Partial<IDailyAffiliations> { interface IPlayerProfileViewingDataResult extends Partial<IDailyAffiliations> {
AccountId: IOid; AccountId: IOid;
DisplayName: string; DisplayName: string;

View File

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

View File

@ -27,15 +27,7 @@ const viewController: RequestHandler = async (req, res) => {
for (const type of Object.keys(ExportEnemies.avatars)) { for (const type of Object.keys(ExportEnemies.avatars)) {
if (!scans.has(type)) scans.add(type); if (!scans.has(type)) scans.add(type);
} }
responseJson.Scans ??= [];
// 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) { for (const type of scans) {
responseJson.Scans.push({ type: type, scans: 9999 }); responseJson.Scans.push({ type: type, scans: 9999 });
} }

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