Compare commits

..

5 Commits
main ... main

Author SHA1 Message Date
Kenya-DK
b72d334eab Merge remote-tracking branch 'origin/main'
All checks were successful
Build / build (20) (pull_request) Successful in 34s
Build / build (18) (pull_request) Successful in 59s
Build / build (22) (pull_request) Successful in 1m10s
2025-02-19 23:04:27 +01:00
Kenya-DK
9accf7a597 fix: use Object.Assign when updating 2025-02-19 23:04:21 +01:00
c9eae22312 merge upstream
All checks were successful
Build / build (18) (pull_request) Successful in 37s
Build / build (20) (pull_request) Successful in 1m3s
Build / build (22) (pull_request) Successful in 57s
2025-02-19 13:11:42 -08:00
Kenya-DK
2d97dee80c fix: better naming
All checks were successful
Build / build (20) (pull_request) Successful in 35s
Build / build (18) (pull_request) Successful in 1m3s
Build / build (22) (pull_request) Successful in 57s
2025-02-19 22:11:17 +01:00
Kenya-DK
6c6403f460 fix: Unable to save settings when accepting trade policy.
All checks were successful
Build / build (22) (pull_request) Successful in 56s
Build / build (20) (pull_request) Successful in 1m13s
Build / build (18) (pull_request) Successful in 36s
2025-02-19 20:29:50 +01:00
275 changed files with 5836 additions and 23290 deletions

View File

@ -1,19 +0,0 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "en-US"
early_access: false
reviews:
profile: "chill"
request_changes_workflow: false
changed_files_summary: false
high_level_summary: false
poem: false
review_status: true
commit_status: false
collapse_walkthrough: false
sequence_diagrams: false
related_prs: false
auto_review:
enabled: true
drafts: false
chat:
auto_reply: true

View File

@ -15,16 +15,17 @@
"@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "warn",
"no-loss-of-precision": "warn", "@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn", "@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-base-to-string": "off", "no-case-declarations": "warn",
"no-case-declarations": "error",
"prettier/prettier": "error", "prettier/prettier": "error",
"@typescript-eslint/semi": "error",
"no-mixed-spaces-and-tabs": "error", "no-mixed-spaces-and-tabs": "error",
"require-await": "off", "require-await": "off",
"@typescript-eslint/require-await": "error" "@typescript-eslint/require-await": "error"

2
.gitattributes vendored
View File

@ -1,4 +1,4 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto eol=lf * text=auto
static/webui/libs/ linguist-vendored static/webui/libs/ linguist-vendored

View File

@ -5,22 +5,17 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
version: [18, 20, 22]
steps: steps:
- name: Checkout - name: Checkout
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: ${{ matrix.version }}
- 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 build
- run: npm run lint:ci - run: npm run lint
- run: npm run prettier
- run: npm run update-translations
- name: Fail if there are uncommitted changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Uncommitted changes detected:"
git status
git --no-pager diff
exit 1
fi

View File

@ -5,7 +5,6 @@ on:
- main - main
jobs: jobs:
docker: docker:
if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Set up Docker buildx - name: Set up Docker buildx

View File

@ -1,4 +1,2 @@
src/routes/api.ts
static/webui/libs/ static/webui/libs/
*.html *.html
*.md

View File

@ -1,3 +0,0 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@ -10,6 +10,8 @@ ENV APP_SKIP_TUTORIAL=true
ENV APP_SKIP_ALL_DIALOGUE=true ENV APP_SKIP_ALL_DIALOGUE=true
ENV APP_UNLOCK_ALL_SCANS=true ENV APP_UNLOCK_ALL_SCANS=true
ENV APP_UNLOCK_ALL_MISSIONS=true ENV APP_UNLOCK_ALL_MISSIONS=true
ENV APP_UNLOCK_ALL_QUESTS=true
ENV APP_COMPLETE_ALL_QUESTS=true
ENV APP_INFINITE_RESOURCES=true ENV APP_INFINITE_RESOURCES=true
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true

View File

@ -2,16 +2,7 @@
More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY) More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY)
## Project Status
This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
## config.json ## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
- `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.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.

View File

@ -1,8 +1,8 @@
@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 reset --hard origin/main git reset --hard origin/main
if exist static\data\0\ ( if exist static\data\0\ (
@ -13,13 +13,12 @@ if exist static\data\0\ (
) )
echo Updating dependencies... echo Updating dependencies...
call npm i --omit=dev call npm i
call npm run build call npm run build
if %errorlevel% == 0 (
call npm run start call npm run start
echo SpaceNinjaServer seems to have crashed. echo SpaceNinjaServer seems to have crashed.
)
:a :a
pause > nul pause > nul
goto a goto a

View File

@ -7,50 +7,25 @@
"myAddress": "localhost", "myAddress": "localhost",
"httpPort": 80, "httpPort": 80,
"httpsPort": 443, "httpsPort": 443,
"NRS": ["localhost"],
"administratorNames": [], "administratorNames": [],
"autoCreateAccount": true, "autoCreateAccount": true,
"skipTutorial": false, "skipTutorial": true,
"skipAllDialogue": false, "skipAllDialogue": true,
"unlockAllScans": false, "unlockAllScans": true,
"unlockAllMissions": false, "unlockAllMissions": true,
"infiniteCredits": false, "infiniteCredits": true,
"infinitePlatinum": false, "infinitePlatinum": true,
"infiniteEndo": false, "infiniteEndo": true,
"infiniteRegalAya": false, "infiniteRegalAya": true,
"infiniteHelminthMaterials": false, "unlockAllShipFeatures": true,
"dontSubtractConsumables": false, "unlockAllShipDecorations": true,
"unlockAllShipFeatures": false, "unlockAllFlavourItems": true,
"unlockAllShipDecorations": false, "unlockAllSkins": true,
"unlockAllFlavourItems": false, "unlockAllCapturaScenes": true,
"unlockAllSkins": false, "universalPolarityEverywhere": true,
"unlockAllCapturaScenes": false, "unlockDoubleCapacityPotatoesEverywhere": true,
"universalPolarityEverywhere": false, "unlockExilusEverywhere": true,
"unlockDoubleCapacityPotatoesEverywhere": false, "unlockArcanesEverywhere": true,
"unlockExilusEverywhere": false, "noDailyStandingLimits": true,
"unlockArcanesEverywhere": false, "spoofMasteryRank": -1
"noDailyStandingLimits": false,
"noDailyFocusLimit": false,
"noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true,
"noDeathMarks": false,
"noKimCooldowns": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"worldState": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true,
"lockTime": 0
}
} }

930
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,41 +4,37 @@
"description": "WF Emulator", "description": "WF Emulator",
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "start": "node --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "build": "tsc && copyfiles static/webui/** build",
"verify": "tsgo --noEmit",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .", "prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js" "update-translations": "cd scripts && node update-translations.js"
}, },
"license": "GNU", "license": "GNU",
"dependencies": { "dependencies": {
"@types/express": "^5", "copyfiles": "^2.4.1",
"@types/morgan": "^1.9.9",
"crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.2.2", "mongoose": "^8.9.4",
"mongoose": "^8.11.0", "warframe-public-export-plus": "^0.5.30",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"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"
}, },
"devDependencies": { "devDependencies": {
"@rxliuli/tsgo": "^2025.3.31", "@types/express": "^5",
"@typescript-eslint/eslint-plugin": "^8.28.0", "@types/morgan": "^1.9.9",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/eslint-plugin": "^7.18",
"eslint": "^8", "@typescript-eslint/parser": "^7.18",
"eslint-plugin-prettier": "^5.2.5", "eslint": "^8.56.0",
"prettier": "^3.5.3", "eslint-plugin-prettier": "^5.2.3",
"morgan": "^1.10.0",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0" "tsconfig-paths": "^4.2.0",
"typescript": ">=4.7.4 <5.6.0"
}, },
"engines": { "engines": {
"node": ">=18.15.0", "node": ">=18.15.0",

View File

@ -1,10 +1,10 @@
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php // Based on http://209.141.38.3/OpenWF/Translations/src/branch/main/update.php
// Converted via ChatGPT-4o // Converted via ChatGPT-4o
const fs = require("fs"); const fs = require("fs");
function extractStrings(content) { function extractStrings(content) {
const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g; const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
let matches; let matches;
const strings = {}; const strings = {};
while ((matches = regex.exec(content)) !== null) { while ((matches = regex.exec(content)) !== null) {
@ -15,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
const sourceStrings = extractStrings(source); const sourceStrings = extractStrings(source);
const sourceLines = source.substring(0, source.length - 1).split("\n"); const sourceLines = source.split("\n");
fs.readdirSync("../static/webui/translations").forEach(file => { fs.readdirSync("../static/webui/translations").forEach(file => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") { if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
} }
}); });
} else { } else if (line.length) {
fs.writeSync(fileHandle, line + "\n"); fs.writeSync(fileHandle, line + "\n");
} }
}); });

View File

@ -15,24 +15,14 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express(); const app = express();
app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
// The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined;
}
next();
});
app.use(bodyParser.raw()); app.use(bodyParser.raw());
app.use(express.json({ limit: "4mb" })); app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text({ limit: "4mb" })); app.use(bodyParser.text());
app.use(requestLogger); app.use(requestLogger);
app.use("/api", apiRouter); app.use("/api", apiRouter);
app.use("/", cacheRouter); app.use("/", cacheRouter);
app.use("/custom", customRouter); app.use("/custom", customRouter);
app.use("/dynamic", dynamicController);
app.use("/:id/dynamic", dynamicController); app.use("/:id/dynamic", dynamicController);
app.use("/pay", payRouter); app.use("/pay", payRouter);
app.use("/stats", statsRouter); app.use("/stats", statsRouter);

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,11 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const abandonLibraryDailyTaskController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
inventory.LibraryActiveDailyTaskInfo = undefined;
await inventory.save();
res.status(200).end();
};

View File

@ -1,46 +0,0 @@
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission,
removeDojoDeco,
removeDojoRoom
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const abortDojoComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IAbortDojoComponentRequest;
if (
!hasAccessToDojo(inventory) ||
!(await hasGuildPermission(
guild,
accountId,
request.DecoId ? GuildPermission.Decorator : GuildPermission.Architect
))
) {
res.json({ DojoRequestStatus: -1 });
return;
}
if (request.DecoId) {
removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else {
await removeDojoRoom(guild, request.ComponentId);
}
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
};
interface IAbortDojoComponentRequest {
DecoType?: string;
ComponentId: string;
DecoId?: string;
}

View File

@ -1,21 +0,0 @@
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const componentId = req.query.componentId as string;
guild.DojoComponents.id(componentId)!.DestructionTime = undefined;
await guild.save();
res.json(await getDojoClient(guild, 0, componentId));
};

View File

@ -1,9 +1,8 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, 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, getRandomInt, getRandomReward, IRngResult } from "@/src/services/rngService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportUpgrades } from "warframe-public-export-plus"; import { ExportUpgrades } from "warframe-public-export-plus";
@ -17,24 +16,83 @@ 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 = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const challenge = getRandomElement(ExportUpgrades[rivenType].availableChallenges!);
const fingerprintChallenge: IRandomModChallenge = {
Type: challenge.fullName,
Progress: 0,
Required: getRandomInt(challenge.countRange[0], challenge.countRange[1])
};
if (Math.random() < challenge.complicationChance) {
const complicationsAsRngResults: IRngResult[] = [];
for (const complication of challenge.complications) {
complicationsAsRngResults.push({
type: complication.fullName,
itemCount: 1,
probability: complication.weight
});
}
fingerprintChallenge.Complication = getRandomReward(complicationsAsRngResults)!.type;
logger.debug(
`riven rolled challenge ${fingerprintChallenge.Type} with complication ${fingerprintChallenge.Complication}`
);
const complication = challenge.complications.find(x => x.fullName == fingerprintChallenge.Complication)!;
fingerprintChallenge.Required *= complication.countMultiplier;
} else {
logger.debug(`riven rolled challenge ${fingerprintChallenge.Type}`);
}
const upgradeIndex = const upgradeIndex =
inventory.Upgrades.push({ inventory.Upgrades.push({
ItemType: rivenType, ItemType: rivenType,
UpgradeFingerprint: JSON.stringify(fingerprint) UpgradeFingerprint: JSON.stringify({ challenge: fingerprintChallenge })
}) - 1; }) - 1;
await inventory.save(); await inventory.save();
// For some reason, in this response, the UpgradeFingerprint is simply a nested object and not a string
res.json({ res.json({
NewMod: { NewMod: inventory.Upgrades[upgradeIndex].toJSON()
UpgradeFingerprint: fingerprint,
ItemType: rivenType,
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id)
}
}); });
}; };
interface IActiveRandomModRequest { interface IActiveRandomModRequest {
ItemType: string; ItemType: string;
} }
interface IRandomModChallenge {
Type: string;
Progress: number;
Required: number;
Complication?: string;
}
const rivenRawToRealWeighted: Record<string, string[]> = {
"/Lotus/Upgrades/Mods/Randomized/RawArchgunRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusArchgunRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawMeleeRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawModularMeleeRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusModularMeleeRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawModularPistolRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusModularPistolRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawPistolRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare"],
"/Lotus/Upgrades/Mods/Randomized/RawRifleRandomMod": ["/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare"],
"/Lotus/Upgrades/Mods/Randomized/RawShotgunRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare"
],
"/Lotus/Upgrades/Mods/Randomized/RawSentinelWeaponRandomMod": [
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusRifleRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusShotgunRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/LotusPistolRandomModRare",
"/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare"
]
};

View File

@ -1,25 +1,16 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IUpdateGlyphRequest } from "@/src/types/requestTypes";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { getInventory } from "@/src/services/inventoryService";
export const addFriendImageController: RequestHandler = async (req, res) => { const addFriendImageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const json = getJSONfromString<IUpdateGlyphRequest>(String(req.body)); const json = getJSONfromString<IUpdateGlyphRequest>(String(req.body));
const inventory = await getInventory(accountId);
await Inventory.updateOne( inventory.ActiveAvatarImageType = json.AvatarImageType;
{ await inventory.save();
accountOwnerId: accountId
},
{
ActiveAvatarImageType: json.AvatarImageType
}
);
res.json({}); res.json({});
}; };
interface IUpdateGlyphRequest { export { addFriendImageController };
AvatarImageType: string;
AvatarImage: string;
}

View File

@ -1,30 +0,0 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const addIgnoredUserController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IAddIgnoredUserRequest>(String(req.body));
const ignoreeAccount = await Account.findOne(
{ DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
"_id"
);
if (ignoreeAccount) {
await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id });
res.json({
Ignored: {
_id: toOid(ignoreeAccount._id),
DisplayName: data.playerName
} satisfies IFriendInfo
});
} else {
res.status(400).end();
}
};
interface IAddIgnoredUserRequest {
playerName: string;
}

View File

@ -1,117 +0,0 @@
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
export const addToAllianceController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
// Check guild has invite permissions in the alliance
const allianceMember = (await AllianceMember.findOne({
allianceId: req.query.allianceId,
guildId: guildMember.guildId
}))!;
if (!(allianceMember.Permissions & GuildPermission.Recruiter)) {
res.status(400).json({ Error: 104 });
return;
}
// Find clan to invite
const payload = getJSONfromString<IAddToAllianceRequest>(String(req.body));
const guilds = await Guild.find(
{
Name:
payload.clanName.indexOf("#") == -1
? new RegExp("^" + regexEscape(payload.clanName) + "#...$")
: payload.clanName
},
"Name"
);
if (guilds.length == 0) {
res.status(400).json({ Error: 101 });
return;
}
if (guilds.length > 1) {
const choices: IGuildChoice[] = [];
for (const guild of guilds) {
choices.push({
OriginalPlatform: 0,
Name: guild.Name
});
}
res.json(choices);
return;
}
// Add clan as a pending alliance member
try {
await AllianceMember.insertOne({
allianceId: req.query.allianceId,
guildId: guilds[0]._id,
Pending: true,
Permissions: 0
});
} catch (e) {
logger.debug(`alliance invite failed due to ${String(e)}`);
res.status(400).json({ Error: 102 });
return;
}
// Send inbox message to founding warlord
// TOVERIFY: Should other warlords get this as well?
// TOVERIFY: Who/what should the sender be?
// TOVERIFY: Should this message be highPriority?
const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!;
await createMessage(invitedClanOwnerMember.accountId, [
{
sndr: getSuffixedName(account),
msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body",
arg: [
{
Key: "THEIR_CLAN",
Tag: senderGuild.Name
},
{
Key: "CLAN",
Tag: guilds[0].Name
},
{
Key: "ALLIANCE",
Tag: alliance.Name
}
],
sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: alliance._id.toString(),
highPriority: true,
acceptAction: "ALLIANCE_INVITE",
declineAction: "ALLIANCE_INVITE",
hasAccountAction: true
}
]);
res.end();
};
interface IAddToAllianceRequest {
clanName: string;
}
interface IGuildChoice {
OriginalPlatform: number;
Name: string;
}

View File

@ -1,105 +0,0 @@
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
export const addToGuildController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as IAddToGuildRequest;
if ("UserName" in payload) {
// Clan recruiter sending an invite
const account = await Account.findOne({ DisplayName: payload.UserName });
if (!account) {
res.status(400).json("Username does not exist");
return;
}
const inventory = await getInventory(account._id.toString(), "Settings");
// TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") {
res.status(400).json("Invite restricted");
return;
}
const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
const senderAccount = await getAccountForRequest(req);
if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission");
}
try {
await GuildMember.insertOne({
accountId: account._id,
guildId: payload.GuildId.$oid,
status: 2 // outgoing invite
});
} catch (e) {
logger.debug(`guild invite failed due to ${String(e)}`);
res.status(400).json("User already invited to clan");
return;
}
const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType");
await createMessage(account._id, [
{
sndr: getSuffixedName(senderAccount),
msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body",
arg: [
{
Key: "clan",
Tag: guild.Name
}
],
sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: payload.GuildId.$oid,
highPriority: true,
acceptAction: "GUILD_INVITE",
declineAction: "GUILD_INVITE",
hasAccountAction: true
}
]);
const member: IGuildMemberClient = {
_id: { $oid: account._id.toString() },
DisplayName: account.DisplayName,
Rank: 7,
Status: 2
};
await fillInInventoryDataForGuildMember(member);
res.json({ NewMember: member });
} else if ("RequestMsg" in payload) {
// Player applying to join a clan
const accountId = await getAccountIdForRequest(req);
try {
await GuildMember.insertOne({
accountId,
guildId: payload.GuildId.$oid,
status: 1, // incoming invite
RequestMsg: payload.RequestMsg,
RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable.
});
} catch (e) {
logger.debug(`guild invite failed due to ${String(e)}`);
res.status(400).send("Already requested");
}
res.end();
} else {
logger.error(`data provided to ${req.path}: ${String(req.body)}`);
res.status(400).end();
}
};
interface IAddToGuildRequest {
UserName?: string;
GuildId: IOid;
RequestMsg?: string;
}

View File

@ -1,178 +0,0 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IOid } from "@/src/types/commonTypes";
import { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
export const artifactTransmutationController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = JSON.parse(String(req.body)) as IArtifactTransmutationRequest;
inventory.RegularCredits -= payload.Cost;
inventory.FusionPoints -= payload.FusionPointCost;
if (payload.RivenTransmute) {
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientSecretItem",
ItemCount: -1
}
]);
payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
});
const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({
ItemType: rivenType,
UpgradeFingerprint: JSON.stringify(fingerprint)
}) - 1;
await inventory.save();
res.json({
NewMods: [
{
ItemId: toOid(inventory.Upgrades[upgradeIndex]._id),
ItemType: rivenType,
UpgradeFingerprint: fingerprint
}
]
});
} else {
const counts: Record<TRarity, number> = {
COMMON: 0,
UNCOMMON: 0,
RARE: 0,
LEGENDARY: 0
};
let forcedPolarity: string | undefined;
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else {
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
}
if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
forcedPolarity = "AP_ATTACK";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
forcedPolarity = "AP_DEFENSE";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/TacticTransmuteCore") {
forcedPolarity = "AP_TACTIC";
}
});
let newModType: string | undefined;
for (const specialModSet of specialModSets) {
if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
newModType = getRandomElement(specialModSet);
break;
}
}
if (!newModType) {
// Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
});
newModType = getRandomWeightedReward(options, weights)!.uniqueName;
}
addMods(inventory, [
{
ItemType: newModType,
ItemCount: 1
}
]);
await inventory.save();
res.json({
NewMods: [
{
ItemType: newModType,
ItemCount: 1
}
]
});
}
};
const getRandomRawRivenType = (): string => {
const pack = ExportBoosterPacks["/Lotus/Types/BoosterPacks/CalendarRivenPack"];
return getRandomWeightedRewardUc(pack.components, pack.rarityWeightsPerRoll[0])!.Item;
};
interface IArtifactTransmutationRequest {
Upgrade: IAgnosticUpgradeClient;
LevelDiff: number;
Consumed: IAgnosticUpgradeClient[];
Cost: number;
FusionPointCost: number;
RivenTransmute?: boolean;
}
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [
[
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
],
[
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
],
[
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
]
];

View File

@ -1,20 +0,0 @@
import { GuildAd } from "@/src/models/guildModel";
import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) {
res.status(400).end();
return;
}
await GuildAd.deleteOne({ GuildId: guild._id });
res.end();
};

View File

@ -1,28 +1,15 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes"; import { IDojoComponentDatabase } from "@/src/types/guildTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const changeDojoRootController: RequestHandler = async (req, res) => { export const changeDojoRootController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const guild = await getGuildForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys"); // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the root.
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
// Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"}
if (req.body) {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId
}
const idToNode: Record<string, INode> = {}; const idToNode: Record<string, INode> = {};
guild.DojoComponents.forEach(x => { guild.DojoComponents!.forEach(x => {
idToNode[x._id.toString()] = { idToNode[x._id.toString()] = {
component: x, component: x,
parent: undefined, parent: undefined,
@ -31,7 +18,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
}); });
let oldRoot: INode | undefined; let oldRoot: INode | undefined;
guild.DojoComponents.forEach(x => { guild.DojoComponents!.forEach(x => {
const node = idToNode[x._id.toString()]; const node = idToNode[x._id.toString()];
if (x.pi) { if (x.pi) {
idToNode[x.pi.toString()].children.push(node); idToNode[x.pi.toString()].children.push(node);
@ -49,19 +36,29 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
newRoot.component.pp = undefined; newRoot.component.pp = undefined;
newRoot.parent = undefined; newRoot.parent = undefined;
// Set/update SortId in top-to-bottom order // Don't even ask me why this is needed because I don't know either
const stack: INode[] = [newRoot]; const stack: INode[] = [newRoot];
let i = 0;
const idMap: Record<string, Types.ObjectId> = {};
while (stack.length != 0) { while (stack.length != 0) {
const top = stack.shift()!; const top = stack.shift()!;
top.component.SortId = new Types.ObjectId(); idMap[top.component._id.toString()] = new Types.ObjectId(
(++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
);
top.children.forEach(x => stack.push(x)); top.children.forEach(x => stack.push(x));
} }
guild.DojoComponents!.forEach(x => {
x._id = idMap[x._id.toString()];
if (x.pi) {
x.pi = idMap[x.pi.toString()];
}
});
logger.debug("New tree:\n" + treeToString(newRoot)); logger.debug("New tree:\n" + treeToString(newRoot));
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0)); res.json(getDojoClient(guild, 0));
}; };
interface INode { interface INode {

View File

@ -1,38 +0,0 @@
import { GuildMember } from "@/src/models/guildModel";
import { getGuildForRequest, hasGuildPermissionEx } from "@/src/services/guildService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const changeGuildRankController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const member = (await GuildMember.findOne({
accountId: accountId,
guildId: req.query.guildId as string
}))!;
const newRank: number = parseInt(req.query.rankChange as string);
const guild = await getGuildForRequest(req);
if (newRank < member.rank || !hasGuildPermissionEx(guild, member, GuildPermission.Promoter)) {
res.status(400).json("Invalid permission");
return;
}
const target = (await GuildMember.findOne({
guildId: req.query.guildId as string,
accountId: req.query.targetId as string
}))!;
target.rank = parseInt(req.query.rankChange as string);
await target.save();
if (newRank == 0) {
// If we just promoted someone else to Founding Warlord, we need to demote ourselves to Warlord.
member.rank = 1;
await member.save();
}
res.json({
_id: req.query.targetId as string,
Rank: newRank
});
};

View File

@ -11,16 +11,12 @@ import {
getInventory, getInventory,
updateCurrency, updateCurrency,
addItem, addItem,
addMiscItems,
addRecipes, addRecipes,
occupySlot, updateCurrencyByAccountId
combineInventoryChanges
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
interface IClaimCompletedRecipeRequest { export interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
} }
@ -41,6 +37,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
// } // }
inventory.PendingRecipes.pull(pendingRecipe._id); inventory.PendingRecipes.pull(pendingRecipe._id);
await inventory.save();
const recipe = getRecipe(pendingRecipe.ItemType); const recipe = getRecipe(pendingRecipe.ItemType);
if (!recipe) { if (!recipe) {
@ -48,41 +45,21 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
if (req.query.cancel) { if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = { const inventory = await getInventory(accountId);
...updateCurrency(inventory, recipe.buildPrice * -1, false) const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false);
}; addMiscItems(inventory, recipe.ingredients);
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.
// Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
res.json({
...currencyChanges,
MiscItems: recipe.ingredients
});
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const inventory = await getInventory(accountId);
inventory.PendingSpectreLoadouts ??= []; inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= []; inventory.SpectreLoadouts ??= [];
@ -100,50 +77,37 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
); );
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
await inventory.save();
} }
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
} }
let InventoryChanges: IInventoryChanges = {}; let InventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
addRecipes(inventory, [ const recipeChanges = [
{ {
ItemType: pendingRecipe.ItemType, ItemType: pendingRecipe.ItemType,
ItemCount: -1 ItemCount: -1
} }
]); ];
InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges };
const inventory = await getInventory(accountId);
addRecipes(inventory, recipeChanges);
await inventory.save();
} }
if (req.query.rush) { if (req.query.rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...updateCurrency(inventory, cost, true) ...(await updateCurrencyByAccountId(recipe.skipBuildTimePrice, true, accountId))
}; };
} }
if (recipe.secretIngredientAction != "SIA_UNBRAND") { const inventory = await getInventory(accountId);
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await addItem( ...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
}; };
}
await inventory.save(); await inventory.save();
res.json({ InventoryChanges, BrandedSuits }); res.json({ InventoryChanges });
} }
}; };

View File

@ -1,31 +0,0 @@
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const rewardQuantity = inventory.LibraryActiveDailyTaskInfo!.RewardQuantity;
const rewardStanding = inventory.LibraryActiveDailyTaskInfo!.RewardStanding;
inventory.LibraryActiveDailyTaskInfo = undefined;
inventory.LibraryAvailableDailyTaskInfo = undefined;
let syndicate = inventory.Affiliations.find(x => x.Tag == "LibrarySyndicate");
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: "LibrarySyndicate", Standing: 0 }) - 1];
}
syndicate.Standing += rewardStanding;
addFusionPoints(inventory, 80 * rewardQuantity);
await inventory.save();
res.json({
RewardItem: "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/RareFusionBundle",
RewardQuantity: rewardQuantity,
StandingAwarded: rewardStanding,
InventoryChanges: {
FusionPoints: 80 * rewardQuantity
}
});
};

View File

@ -7,8 +7,6 @@ export const clearDialogueHistoryController: RequestHandler = async (req, res) =
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IClearDialogueRequest; const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) { if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
inventory.DialogueHistory.Resets ??= 0;
inventory.DialogueHistory.Resets += 1;
for (const dialogueName of request.Dialogues) { for (const dialogueName of request.Dialogues) {
const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName); const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
if (index != -1) { if (index != -1) {

View File

@ -1,6 +0,0 @@
import { RequestHandler } from "express";
// example req.body: {"NewEpisodeReward":true,"crossPlaySetting":"ENABLED"}
export const clearNewEpisodeRewardController: RequestHandler = (_req, res) => {
res.status(200).end();
};

View File

@ -1,41 +0,0 @@
import { 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 = {};
let dayIndex = 0;
for (const day of currentSeason.Days) {
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") {
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) {
if (day.events.length != 0) {
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}`);
}
}
break;
}
++dayIndex;
}
}
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
CalendarProgress: inventory.CalendarProgress
});
};

View File

@ -1,45 +0,0 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper";
import { ExportUpgrades } from "warframe-public-export-plus";
export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = getJSONfromString<ICompleteRandomModChallengeRequest>(String(req.body));
let inventoryChanges: IInventoryChanges = {};
// Remove 20 plat or riven cipher
if ((req.query.p as string) == "1") {
inventoryChanges = { ...updateCurrency(inventory, 20, true) };
} else {
const miscItemChanges: IMiscItem[] = [
{
ItemType: "/Lotus/Types/Items/MiscItems/RivenIdentifier",
ItemCount: -1
}
];
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
// Update riven fingerprint to a randomised unveiled state
const upgrade = inventory.Upgrades.id(request.ItemId)!;
const meta = ExportUpgrades[upgrade.ItemType];
upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta));
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
Fingerprint: upgrade.UpgradeFingerprint
});
};
interface ICompleteRandomModChallengeRequest {
ItemId: string;
}

View File

@ -1,37 +0,0 @@
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const confirmAllianceInvitationController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const accountId = await getAccountIdForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
const allianceMember = await AllianceMember.findOne({
allianceId: req.query.allianceId,
guildId: guildMember.guildId
});
if (!allianceMember || !allianceMember.Pending) {
res.status(400);
return;
}
allianceMember.Pending = false;
const guild = (await Guild.findById(guildMember.guildId))!;
guild.AllianceId = allianceMember.allianceId;
await Promise.all([allianceMember.save(), guild.save()]);
// Give client the new alliance data which uses "AllianceId" instead of "_id" in this response
const alliance = (await Alliance.findById(allianceMember.allianceId))!;
const { _id, ...rest } = await getAllianceClient(alliance, guild);
res.json({
AllianceId: _id,
...rest
});
};

View File

@ -1,118 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import {
deleteGuild,
getGuildClient,
giveClanKey,
hasGuildPermission,
removeDojoKeyItems
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
// GET request: A player accepting an invite they got in their inbox.
export const confirmGuildInvitationGetController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const invitedGuildMember = await GuildMember.findOne({
accountId: account._id,
guildId: req.query.clanId as string
});
if (invitedGuildMember && invitedGuildMember.status == 2) {
let inventoryChanges: IInventoryChanges = {};
// If this account is already in a guild, we need to do cleanup first.
const guildMember = await GuildMember.findOneAndDelete({ accountId: account._id, status: 0 });
if (guildMember) {
const inventory = await getInventory(account._id.toString(), "LevelKeys Recipes");
inventoryChanges = removeDojoKeyItems(inventory);
await inventory.save();
if (guildMember.rank == 0) {
await deleteGuild(guildMember.guildId);
}
}
// Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates.
invitedGuildMember.status = 0;
await invitedGuildMember.save();
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId: account._id, status: 1 });
// Update inventory of new member
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory, inventoryChanges);
await inventory.save();
const guild = (await Guild.findById(req.query.clanId as string))!;
// Add join to clan log
guild.RosterActivity ??= [];
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 6,
details: getSuffixedName(account)
});
await guild.save();
res.json({
...(await getGuildClient(guild, account._id.toString())),
InventoryChanges: inventoryChanges
});
} else {
res.end();
}
};
// POST request: Clan representative accepting invite(s).
export const confirmGuildInvitationPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission");
return;
}
const payload = getJSONfromString<{ userId: string }>(String(req.body));
const filter: { accountId?: string; status: number } = { status: 1 };
if (payload.userId != "all") {
filter.accountId = payload.userId;
}
const guildMembers = await GuildMember.find(filter);
const newMembers: string[] = [];
for (const guildMember of guildMembers) {
guildMember.status = 0;
guildMember.RequestMsg = undefined;
guildMember.RequestExpiry = undefined;
await guildMember.save();
// Remove other pending applications for this account
await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
// Update inventory of new member
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory);
await inventory.save();
// Add join to clan log
const account = (await Account.findOne({ _id: guildMember.accountId }))!;
guild.RosterActivity ??= [];
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 6,
details: getSuffixedName(account)
});
newMembers.push(account._id.toString());
}
await guild.save();
res.json({
NewMembers: newMembers
});
};

View File

@ -1,67 +0,0 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const contributeGuildClassController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<IContributeGuildClassRequest>(String(req.body));
const guild = (await Guild.findById(payload.GuildId))!;
// First contributor initiates ceremony and locks the pending class.
if (!guild.CeremonyContributors) {
guild.CeremonyContributors = [];
guild.CeremonyClass = guildXpToClass(guild.XP);
guild.CeremonyEndo = 0;
for (let i = guild.Class; i != guild.CeremonyClass; ++i) {
guild.CeremonyEndo += (i + 1) * 1000;
}
guild.ClassChanges ??= [];
guild.ClassChanges.push({
dateTime: new Date(),
entryType: 13,
details: guild.CeremonyClass
});
}
guild.CeremonyContributors.push(new Types.ObjectId(accountId));
await checkClanAscensionHasRequiredContributors(guild);
await guild.save();
// Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints");
addFusionPoints(inventory, guild.CeremonyEndo!);
await inventory.save();
res.json({
NumContributors: guild.CeremonyContributors.length,
FusionPointReward: guild.CeremonyEndo,
Class: guild.Class,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined
});
};
interface IContributeGuildClassRequest {
GuildId: string;
RequiredContributors: number;
}
const guildXpToClass = (xp: number): number => {
const cummXp = [
0, 11000, 34000, 69000, 114000, 168000, 231000, 302000, 381000, 68000, 563000, 665000, 774000, 891000
];
let highest = 0;
for (let i = 0; i != cummXp.length; ++i) {
if (xp < cummXp[i]) {
break;
}
highest = i;
}
return highest;
};

View File

@ -1,168 +0,0 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
addGuildMemberMiscItemContribution,
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
processDojoBuildMaterialsGathered,
scaleRequiredCount,
setDojoRoomLogFunded
} from "@/src/services/guildService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IDojoContributable, IGuildMemberDatabase } from "@/src/types/guildTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus";
interface IContributeToDojoComponentRequest {
ComponentId: string;
DecoId?: string;
DecoType?: string;
IngredientContributions: IMiscItem[];
RegularCredits: number;
VaultIngredientContributions: IMiscItem[];
VaultCredits: number;
}
export const contributeToDojoComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
// Any clan member should have permission to contribute although notably permission is denied if they have not crafted the dojo key and were simply invited in.
if (!hasAccessToDojo(inventory)) {
res.json({ DojoRequestStatus: -1 });
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
const inventoryChanges: IInventoryChanges = {};
if (!component.CompletionTime) {
// Room is in "Collecting Materials" state
if (request.DecoId) {
throw new Error("attempt to contribute to a deco in an unfinished room?!");
}
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, component);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (component.CompletionTime) {
setDojoRoomLogFunded(guild, component);
}
} else {
// Room is past "Collecting Materials"
if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, deco);
}
}
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges
});
};
const processContribution = (
guild: TGuildDatabaseDocument,
guildMember: IGuildMemberDatabase,
request: IContributeToDojoComponentRequest,
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
meta: IDojoBuild,
component: IDojoContributable
): void => {
component.RegularCredits ??= 0;
if (request.RegularCredits) {
component.RegularCredits += request.RegularCredits;
inventoryChanges.RegularCredits = -request.RegularCredits;
updateCurrency(inventory, request.RegularCredits, false);
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += request.RegularCredits;
}
if (request.VaultCredits) {
component.RegularCredits += request.VaultCredits;
guild.VaultRegularCredits! -= request.VaultCredits;
}
if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) {
guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price);
component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
}
component.MiscItems ??= [];
if (request.VaultIngredientContributions.length) {
for (const ingredientContribution of request.VaultIngredientContributions) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType);
if (componentMiscItem) {
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
) {
ingredientContribution.ItemCount =
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
}
componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else {
component.MiscItems.push(ingredientContribution);
}
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == ingredientContribution.ItemType)!;
vaultMiscItem.ItemCount -= ingredientContribution.ItemCount;
}
}
if (request.IngredientContributions.length) {
const miscItemChanges: IMiscItem[] = [];
for (const ingredientContribution of request.IngredientContributions) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredientContribution.ItemType);
if (componentMiscItem) {
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
) {
ingredientContribution.ItemCount =
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
}
componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else {
component.MiscItems.push(ingredientContribution);
}
miscItemChanges.push({
ItemType: ingredientContribution.ItemType,
ItemCount: ingredientContribution.ItemCount * -1
});
addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let fullyFunded = true;
for (const ingredient of meta.ingredients) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType);
if (
!componentMiscItem ||
componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
fullyFunded = false;
break;
}
}
if (fullyFunded) {
component.CompletionTime = new Date(Date.now() + meta.time * 1000);
processDojoBuildMaterialsGathered(guild, meta);
}
}
};

View File

@ -1,113 +0,0 @@
import {
Alliance,
Guild,
GuildMember,
TGuildDatabaseDocument,
TGuildMemberDatabaseDocument
} from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addGuildMemberShipDecoContribution,
addVaultFusionTreasures,
addVaultMiscItems,
addVaultShipDecos,
getGuildForRequestEx
} from "@/src/services/guildService";
import {
addFusionTreasures,
addMiscItems,
addShipDecorations,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const contributeToVaultController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures");
const request = JSON.parse(String(req.body)) as IContributeToVaultRequest;
if (request.Alliance) {
const guild = await getGuildForRequestEx(req, inventory);
const alliance = (await Alliance.findById(guild.AllianceId!))!;
alliance.VaultRegularCredits ??= 0;
alliance.VaultRegularCredits += request.RegularCredits;
if (request.FromVault) {
guild.VaultRegularCredits! -= request.RegularCredits;
await Promise.all([guild.save(), alliance.save()]);
} else {
updateCurrency(inventory, request.RegularCredits, false);
await Promise.all([inventory.save(), alliance.save()]);
}
res.end();
return;
}
let guild: TGuildDatabaseDocument;
let guildMember: TGuildMemberDatabaseDocument | undefined;
if (request.GuildVault) {
guild = (await Guild.findById(request.GuildVault))!;
} else {
guild = await getGuildForRequestEx(req, inventory);
guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed"
))!;
}
if (request.RegularCredits) {
updateCurrency(inventory, request.RegularCredits, false);
guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += request.RegularCredits;
if (guildMember) {
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += request.RegularCredits;
}
}
if (request.MiscItems.length) {
addVaultMiscItems(guild, request.MiscItems);
for (const item of request.MiscItems) {
if (guildMember) {
addGuildMemberMiscItemContribution(guildMember, item);
}
addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
if (request.ShipDecorations.length) {
addVaultShipDecos(guild, request.ShipDecorations);
for (const item of request.ShipDecorations) {
if (guildMember) {
addGuildMemberShipDecoContribution(guildMember, item);
}
addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
if (request.FusionTreasures.length) {
addVaultFusionTreasures(guild, request.FusionTreasures);
for (const item of request.FusionTreasures) {
addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
const promises: Promise<unknown>[] = [guild.save(), inventory.save()];
if (guildMember) {
promises.push(guildMember.save());
}
await Promise.all(promises);
res.end();
};
interface IContributeToVaultRequest {
RegularCredits: number;
MiscItems: IMiscItem[];
ShipDecorations: ITypeCount[];
FusionTreasures: IFusionTreasure[];
Alliance?: boolean;
FromVault?: boolean;
GuildVault?: string;
}

View File

@ -1,50 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const createAllianceController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!;
if (guild.AllianceId) {
res.status(400).send("Guild is already in an alliance").end();
return;
}
const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!;
if (guildMember.rank > 1) {
res.status(400).send("Invalid permission").end();
return;
}
const data = getJSONfromString<ICreateAllianceRequest>(String(req.body));
const alliance = new Alliance({ Name: data.allianceName });
try {
await alliance.save();
} catch (e) {
res.status(400).send("Alliance name already in use").end();
return;
}
guild.AllianceId = alliance._id;
await Promise.all([
guild.save(),
AllianceMember.insertOne({
allianceId: alliance._id,
guildId: guild._id,
Pending: false,
Permissions:
GuildPermission.Ruler |
GuildPermission.Promoter |
GuildPermission.Recruiter |
GuildPermission.Treasurer |
GuildPermission.ChatModerator
})
]);
res.json(await getAllianceClient(alliance, guild));
};
interface ICreateAllianceRequest {
allianceName: string;
}

View File

@ -1,42 +1,35 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } 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 { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { createUniqueClanName, getGuildClient, giveClanKey } from "@/src/services/guildService"; import { Guild } from "@/src/models/guildModel";
import { 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 accountId = await getAccountIdForRequest(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
await GuildMember.deleteMany({ accountId, status: 1 });
// Create guild on database // Create guild on database
const guild = new Guild({ const guild = new Guild({
Name: await createUniqueClanName(payload.guildName) Name: payload.guildName
}); });
await guild.save(); await guild.save();
// Create guild member on database // Update inventory
await GuildMember.insertOne({ const inventory = await Inventory.findOne({ accountOwnerId: accountId });
accountId: accountId, if (inventory) {
guildId: guild._id, // Set GuildId
status: 0,
rank: 0
});
const inventory = await getInventory(accountId, "GuildId LevelKeys Recipes");
inventory.GuildId = guild._id; inventory.GuildId = guild._id;
const inventoryChanges: IInventoryChanges = {};
giveClanKey(inventory, inventoryChanges);
await inventory.save();
res.json({ // Give clan key (TODO: This should only be a blueprint)
...(await getGuildClient(guild, accountId)), inventory.LevelKeys.push({
InventoryChanges: inventoryChanges ItemType: "/Lotus/Types/Keys/DojoKey",
ItemCount: 1
}); });
await inventory.save();
}
res.json(guild);
}; };
interface ICreateGuildRequest { interface ICreateGuildRequest {

View File

@ -19,7 +19,7 @@ export const creditsController: RequestHandler = async (req, res) => {
response.RegularCredits = 999999999; response.RegularCredits = 999999999;
} }
if (config.infinitePlatinum) { if (config.infinitePlatinum) {
response.PremiumCreditsFree = 0; response.PremiumCreditsFree = 999999999;
response.PremiumCredits = 999999999; response.PremiumCredits = 999999999;
} }

View File

@ -1,28 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
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();
res.json({
crewMemberId: data.crewMember.ItemId.$oid,
NemesisFingerprint: data.crewMember.NemesisFingerprint
});
};
interface ICrewMembersRequest {
crewMember: ICrewMemberClient;
}

View File

@ -1,84 +0,0 @@
import {
addCrewShipSalvagedWeaponSkin,
addCrewShipRawSalvage,
getInventory,
addEquipment
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(
accountId,
"CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage"
);
const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
const inventoryChanges: IInventoryChanges = {};
if (payload.ItemType in ExportCustoms) {
const meta = ExportCustoms[payload.ItemType];
let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] };
if (meta.subroutines) {
upgradeFingerprint = {
SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1),
...upgradeFingerprint
};
}
for (const upgrade of meta.randomisedUpgrades!) {
upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
addCrewShipSalvagedWeaponSkin(
inventory,
payload.ItemType,
JSON.stringify(upgradeFingerprint),
inventoryChanges
);
} else {
const meta = ExportRailjackWeapons[payload.ItemType];
let defaultOverwrites: Partial<IEquipmentDatabase> | undefined;
if (meta.defaultUpgrades?.[0]) {
const upgradeType = meta.defaultUpgrades[0].ItemType;
const upgradeMeta = ExportUpgrades[upgradeType];
const buffs: IFingerprintStat[] = [];
for (const buff of upgradeMeta.upgradeEntries!) {
buffs.push({
Tag: buff.tag,
Value: Math.trunc(Math.random() * 0x40000000)
});
}
defaultOverwrites = {
UpgradeType: upgradeType,
UpgradeFingerprint: JSON.stringify({
compat: payload.ItemType,
buffs
} satisfies IInnateDamageFingerprint)
};
}
addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges);
}
inventoryChanges.CrewShipRawSalvage = [
{
ItemType: payload.ItemType,
ItemCount: -1
}
];
addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipIdentifySalvageRequest {
ItemType: string;
}

View File

@ -1,48 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => {
const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body));
const guild = (await Guild.findById(data.g, "DojoComponents"))!;
const component = guild.DojoComponents.id(data.c)!;
if (req.query.act == "f") {
res.json({
results: component.Leaderboard ?? []
});
} else if (req.query.act == "p") {
const account = await getAccountForRequest(req);
component.Leaderboard ??= [];
const entry = component.Leaderboard.find(x => x.n == account.DisplayName);
if (entry) {
entry.s = data.s!;
} else {
component.Leaderboard.push({
s: data.s!,
n: account.DisplayName,
r: 0
});
}
component.Leaderboard.sort((a, b) => a.s - b.s); // In this case, the score is the time in milliseconds, so smaller is better.
if (component.Leaderboard.length > 10) {
component.Leaderboard.shift();
}
let r = 0;
for (const entry of component.Leaderboard) {
entry.r = ++r;
}
await guild.save();
res.status(200).end();
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown customObstacleCourseLeaderboard act: ${String(req.query.act)}`);
}
};
interface ICustomObstacleCourseLeaderboardRequest {
g: string;
c: string;
s?: number; // act=p
}

View File

@ -1,21 +0,0 @@
import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission, IGuildRank } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const customizeGuildRanksController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const guild = await getGuildForRequest(req);
const payload = JSON.parse(String(req.body)) as ICustomizeGuildRanksRequest;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Ruler))) {
res.status(400).json("Invalid permission");
return;
}
guild.Ranks = payload.GuildRanks;
await guild.save();
res.end();
};
interface ICustomizeGuildRanksRequest {
GuildRanks: IGuildRank[];
}

View File

@ -1,17 +0,0 @@
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const declineAllianceInviteController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const accountId = await getAccountIdForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId });
res.end();
};

View File

@ -1,14 +0,0 @@
import { GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const declineGuildInviteController: RequestHandler = async (req, res) => {
const accountId = await getAccountForRequest(req);
await GuildMember.deleteOne({
accountId: accountId,
guildId: req.query.clanId as string
});
res.end();
};

View File

@ -1,33 +0,0 @@
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission,
removeDojoDeco
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
removeDojoDeco(guild, request.ComponentId, request.DecoId);
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
};
interface IDestroyDojoDecoRequest {
DecoType: string;
ComponentId: string;
DecoId: string;
}

View File

@ -1,67 +0,0 @@
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const divvyAllianceVaultController: RequestHandler = async (req, res) => {
// Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits.
if (req.query.credits == "1") {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).end();
return;
}
// Check guild has treasurer permissions in the alliance
const allianceMember = (await AllianceMember.findOne({
allianceId: req.query.allianceId,
guildId: guildMember.guildId
}))!;
if (!(allianceMember.Permissions & GuildPermission.Treasurer)) {
res.status(400).end();
return;
}
const allianceMembers = await AllianceMember.find({ allianceId: req.query.allianceId });
const memberCounts: Record<string, number> = {};
let totalMembers = 0;
await parallelForeach(allianceMembers, async allianceMember => {
const memberCount = await GuildMember.countDocuments({
guildId: allianceMember.guildId
});
memberCounts[allianceMember.guildId.toString()] = memberCount;
totalMembers += memberCount;
});
logger.debug(`alliance has ${totalMembers} members between all its clans`);
const alliance = (await Alliance.findById(allianceMember.allianceId, "VaultRegularCredits"))!;
if (alliance.VaultRegularCredits) {
let creditsHandedOutInTotal = 0;
await parallelForeach(allianceMembers, async allianceMember => {
const memberCount = memberCounts[allianceMember.guildId.toString()];
const cutPercentage = memberCount / totalMembers;
const creditsToHandOut = Math.trunc(alliance.VaultRegularCredits! * cutPercentage);
logger.debug(
`${allianceMember.guildId.toString()} has ${memberCount} member(s) = ${Math.trunc(cutPercentage * 100)}% of alliance; giving ${creditsToHandOut} credit(s)`
);
if (creditsToHandOut != 0) {
await Guild.updateOne(
{ _id: allianceMember.guildId },
{ $inc: { VaultRegularCredits: creditsToHandOut } }
);
creditsHandedOutInTotal += creditsToHandOut;
}
});
alliance.VaultRegularCredits -= creditsHandedOutInTotal;
logger.debug(
`handed out ${creditsHandedOutInTotal} credits; alliance vault now has ${alliance.VaultRegularCredits} credit(s)`
);
}
await alliance.save();
}
res.end();
};

View File

@ -1,76 +0,0 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IDojoContributable } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus";
interface IDojoComponentRushRequest {
DecoType?: string;
DecoId?: string;
ComponentId: string;
Amount: number;
VaultAmount: number;
AllianceVaultAmount: number;
}
export const dojoComponentRushController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
if (!hasAccessToDojo(inventory)) {
res.json({ DojoRequestStatus: -1 });
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const request = JSON.parse(String(req.body)) as IDojoComponentRushRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
let platinumDonated = request.Amount;
const inventoryChanges = updateCurrency(inventory, request.Amount, true);
if (request.VaultAmount) {
platinumDonated += request.VaultAmount;
guild.VaultPremiumCredits! -= request.VaultAmount;
}
if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(guild, deco, meta, platinumDonated);
} else {
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(guild, component, meta, platinumDonated);
const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
if (entry) {
entry.dateTime = component.CompletionTime!;
}
}
const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!;
guildMember.PremiumCreditsContributed ??= 0;
guildMember.PremiumCreditsContributed += request.Amount;
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges
});
};
const processContribution = (
guild: TGuildDatabaseDocument,
component: IDojoContributable,
meta: IDojoBuild,
platinumDonated: number
): void => {
const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice);
const fullDurationSeconds = meta.time;
const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost;
component.CompletionTime = new Date(
component.CompletionTime!.getTime() - secondsPerPlatinum * platinumDonated * 1000
);
component.RushPlatinum ??= 0;
component.RushPlatinum += platinumDonated;
};

View File

@ -1,11 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL.
export const dojoController: RequestHandler = (_req, res) => { export const dojoController: RequestHandler = (_req, res) => {
res.json("-1"); // Tell client to use authorised request. res.json("-1"); // Tell client to use authorised request.
}; };
export const setDojoURLController: RequestHandler = (_req, res) => {
res.end();
};

View File

@ -1,143 +1,7 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { config } from "@/src/services/configService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { fromStoreItem } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomInt, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus";
export const dronesController: RequestHandler = async (req, res) => { const dronesController: RequestHandler = (_req, res) => {
const accountId = await getAccountIdForRequest(req);
if ("GetActive" in req.query) {
const inventory = await getInventory(accountId, "Drones");
const activeDrones: IActiveDrone[] = [];
for (const drone of inventory.Drones) {
if (drone.DeployTime) {
activeDrones.push({
DeployTime: toMongoDate(drone.DeployTime),
System: drone.System!,
ItemId: toOid(drone._id),
ItemType: drone.ItemType,
CurrentHP: drone.CurrentHP,
DamageTime: toMongoDate(drone.DamageTime!),
PendingDamage: drone.PendingDamage!,
Resources: [
{
ItemType: drone.ResourceType!,
BinTotal: drone.ResourceCount!,
StartTime: toMongoDate(drone.DeployTime)
}
]
});
}
}
res.json({
ActiveDrones: activeDrones
});
} else if ("droneId" in req.query && "systemIndex" in req.query) {
const inventory = await getInventory(accountId, "Drones");
const drone = inventory.Drones.id(req.query.droneId as string)!;
const droneMeta = ExportDrones[drone.ItemType];
drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date();
if (drone.RepairStart) {
const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000;
const hpPerMinute = droneMeta.repairRate / 60;
drone.CurrentHP = Math.min(drone.CurrentHP + Math.round(repairMinutes * hpPerMinute), droneMeta.durability);
drone.RepairStart = undefined;
}
drone.System = parseInt(req.query.systemIndex as string);
const system = ExportSystems[drone.System - 1];
drone.DamageTime = config.instantResourceExtractorDrones
? new Date()
: new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage =
!config.noResourceExtractorDronesDamage && Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;
//logger.debug(`drone rolled`, resource);
drone.ResourceType = fromStoreItem(resource.StoreItem);
const resourceMeta = ExportResources[drone.ResourceType];
if (resourceMeta.pickupQuantity) {
const pickupsToCollect = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
drone.ResourceCount = 0;
for (let i = 0; i != pickupsToCollect; ++i) {
drone.ResourceCount += getRandomInt(
resourceMeta.pickupQuantity.minValue,
resourceMeta.pickupQuantity.maxValue
);
}
} else {
drone.ResourceCount = 1;
}
await inventory.save();
res.json({}); res.json({});
} else if ("collectDroneId" in req.query) {
const inventory = await getInventory(accountId);
const drone = inventory.Drones.id(req.query.collectDroneId as string)!;
if (new Date() >= drone.DamageTime!) {
drone.CurrentHP -= drone.PendingDamage!;
drone.RepairStart = new Date();
}
const inventoryChanges: IInventoryChanges = {};
if (drone.CurrentHP <= 0) {
inventory.RegularCredits += 100;
inventoryChanges.RegularCredits = 100;
inventory.Drones.pull({ _id: req.query.collectDroneId as string });
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: req.query.collectDroneId }
}
];
} else {
const completionTime = drone.DeployTime!.getTime() + ExportDrones[drone.ItemType].fillRate * 3600_000;
if (Date.now() >= completionTime) {
const miscItemChanges = [
{
ItemType: drone.ResourceType!,
ItemCount: drone.ResourceCount!
}
];
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
drone.DeployTime = undefined;
drone.System = undefined;
drone.DamageTime = undefined;
drone.PendingDamage = undefined;
drone.ResourceType = undefined;
drone.ResourceCount = undefined;
inventoryChanges.Drones = [drone.toJSON<IDroneClient>()];
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
} else {
throw new Error(`drones.php query not handled`);
}
}; };
interface IActiveDrone { export { dronesController };
DeployTime: IMongoDate;
System: number;
ItemId: IOid;
ItemType: string;
CurrentHP: number;
DamageTime: IMongoDate;
PendingDamage: number;
Resources: {
ItemType: string;
BinTotal: number;
StartTime: IMongoDate;
}[];
}

View File

@ -1,71 +0,0 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const entratiLabConquestModeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(
accountId,
"EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission"
);
const body = getJSONfromString<IEntratiLabConquestModeRequest>(String(req.body));
if (!inventory.EntratiVaultCountResetDate || Date.now() >= inventory.EntratiVaultCountResetDate.getTime()) {
const EPOCH = 1734307200 * 1000; // Mondays, amirite?
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
inventory.EntratiVaultCountLastPeriod = 0;
inventory.EntratiVaultCountResetDate = new Date(weekEnd);
if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestUnlocked = 0;
inventory.EntratiLabConquestCacheScoreMission = 0;
inventory.EntratiLabConquestActiveFrameVariants = [];
}
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestUnlocked = 0;
inventory.EchoesHexConquestCacheScoreMission = 0;
inventory.EchoesHexConquestActiveFrameVariants = [];
inventory.EchoesHexConquestActiveStickers = [];
}
}
if (body.BuyMode) {
inventory.EntratiVaultCountLastPeriod! += 2;
if (body.IsEchoesDeepArchemedea) {
inventory.EchoesHexConquestUnlocked = 1;
} else {
inventory.EntratiLabConquestUnlocked = 1;
}
}
if (body.IsEchoesDeepArchemedea) {
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestActiveFrameVariants = body.EchoesHexConquestActiveFrameVariants!;
inventory.EchoesHexConquestActiveStickers = body.EchoesHexConquestActiveStickers!;
}
} else {
if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestActiveFrameVariants = body.EntratiLabConquestActiveFrameVariants!;
}
}
await inventory.save();
res.json({
EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate),
EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod,
EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked,
EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission,
EchoesHexConquestUnlocked: inventory.EchoesHexConquestUnlocked,
EchoesHexConquestCacheScoreMission: inventory.EchoesHexConquestCacheScoreMission
});
};
interface IEntratiLabConquestModeRequest {
BuyMode?: number;
IsEchoesDeepArchemedea?: number;
EntratiLabConquestUnlocked?: number;
EntratiLabConquestActiveFrameVariants?: string[];
EchoesHexConquestUnlocked?: number;
EchoesHexConquestActiveFrameVariants?: string[];
EchoesHexConquestActiveStickers?: string[];
}

View File

@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 })) recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
); );
const item = inventory[payload.Category].id(req.query.ItemId as string)!; const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features ??= 0; item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS; item.Features |= EquipmentFeatures.INCARNON_GENESIS;
@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
} }
]); ]);
const item = inventory[payload.Category].id(req.query.ItemId as string)!; const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS; item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
} else { } else {
throw new Error(`unexpected evolve weapon action: ${payload.Action}`); throw new Error(`unexpected evolve weapon action: ${payload.Action}`);

View File

@ -1,9 +1,10 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus"; import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => { export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -30,15 +31,32 @@ 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);
let affiliationMod; if (gainedStanding && syndicateTag) {
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding); let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
}
const syndicateMeta = ExportSyndicates[syndicateTag];
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
syndicate.Standing += gainedStanding;
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
MiscItems: miscItemChanges MiscItems: miscItemChanges
}, },
SyndicateTag: syndicateTag, SyndicateTag: syndicateTag,
StandingChange: affiliationMod?.Standing || 0 StandingChange: gainedStanding
}); });
}; };

View File

@ -1,11 +1,10 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, addEquipment } from "@/src/services/inventoryService";
import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, TFocusPolarity, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { ExportFocusUpgrades } from "warframe-public-export-plus";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const focusController: RequestHandler = async (req, res) => { export const focusController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -18,8 +17,8 @@ export const focusController: RequestHandler = async (req, res) => {
case FocusOperation.InstallLens: { case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest; const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const item = inventory[request.Category].id(request.WeaponId); for (const item of inventory[request.Category]) {
if (item) { if (item._id.toString() == request.WeaponId) {
item.FocusLens = request.LensType; item.FocusLens = request.LensType;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -27,6 +26,8 @@ export const focusController: RequestHandler = async (req, res) => {
ItemCount: -1 ItemCount: -1
} satisfies IMiscItem } satisfies IMiscItem
]); ]);
break;
}
} }
await inventory.save(); await inventory.save();
res.json({ res.json({
@ -54,16 +55,9 @@ export const focusController: RequestHandler = async (req, res) => {
} }
case FocusOperation.ActivateWay: { case FocusOperation.ActivateWay: {
const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType; const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
const inventory = await getInventory(accountId);
await Inventory.updateOne( inventory.FocusAbility = focusType;
{ await inventory.save();
accountOwnerId: accountId
},
{
FocusAbility: focusType
}
);
res.end(); res.end();
break; break;
} }
@ -104,15 +98,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 inventory = await getInventory(accountId); const parts: string[] = [
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip", "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
] ];
}); const inventory = await getInventory(accountId);
occupySlot(inventory, InventorySlot.AMPS, false); const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
await inventory.save(); await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);
break; break;

View File

@ -23,11 +23,12 @@ export const fusionTreasuresController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IFusionTreasureRequest; const request = JSON.parse(String(req.body)) as IFusionTreasureRequest;
// Swap treasures
const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1); const oldTreasure = parseFusionTreasure(request.oldTreasureName, -1);
const newTreasure = parseFusionTreasure(request.newTreasureName, 1); const newTreasure = parseFusionTreasure(request.newTreasureName, 1);
const fusionTreasureChanges = [oldTreasure, newTreasure];
addFusionTreasures(inventory, fusionTreasureChanges); // Swap treasures
addFusionTreasures(inventory, [oldTreasure]);
addFusionTreasures(inventory, [newTreasure]);
// Remove consumed stars // Remove consumed stars
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
@ -44,9 +45,5 @@ export const fusionTreasuresController: RequestHandler = async (req, res) => {
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
await inventory.save(); await inventory.save();
// The response itself is the inventory changes for this endpoint. res.end();
res.json({
MiscItems: miscItemChanges,
FusionTreasures: fusionTreasureChanges
});
}; };

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

@ -10,7 +10,8 @@ import { IGenericUpdate } from "@/src/types/genericUpdate";
const genericUpdateController: RequestHandler = async (request, response) => { const genericUpdateController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
const update = getJSONfromString<IGenericUpdate>(String(request.body)); const update = getJSONfromString<IGenericUpdate>(String(request.body));
response.json(await updateGeneric(update, accountId)); await updateGeneric(update, accountId);
response.json(update);
}; };
export { genericUpdateController }; export { genericUpdateController };

View File

@ -1,25 +1,7 @@
import { Alliance, Guild } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getAllianceController: RequestHandler = async (req, res) => { const getAllianceController: RequestHandler = (_req, res) => {
const accountId = await getAccountIdForRequest(req); res.sendStatus(200);
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!;
if (guild.AllianceId) {
const alliance = (await Alliance.findById(guild.AllianceId))!;
res.json(await getAllianceClient(alliance, guild));
return;
}
}
res.end();
}; };
/*interface IGetAllianceRequest { export { getAllianceController };
memberCount: number;
clanLeaderName: string;
clanLeaderId: string;
}*/

View File

@ -1,6 +1,5 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
// POST with {} instead of GET as of 38.5.0
const getFriendsController = (_request: Request, response: Response): void => { const getFriendsController = (_request: Request, response: Response): void => {
response.writeHead(200, { response.writeHead(200, {
//Connection: "keep-alive", //Connection: "keep-alive",

View File

@ -1,19 +0,0 @@
import { GuildMember } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IGuildMemberClient } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const getGuildContributionsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const guildId = (await getInventory(accountId, "GuildId")).GuildId;
const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!;
res.json({
_id: { $oid: req.query.buddyId as string },
RegularCreditsContributed: guildMember.RegularCreditsContributed,
PremiumCreditsContributed: guildMember.PremiumCreditsContributed,
MiscItemsContributed: guildMember.MiscItemsContributed,
ConsumablesContributed: [], // ???
ShipDecorationsContributed: guildMember.ShipDecorationsContributed
} satisfies Partial<IGuildMemberClient>);
};

View File

@ -1,32 +1,73 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
export const getGuildController: RequestHandler = async (req, res) => { const getGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId"); const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (!inventory) {
res.status(400).json({ error: "inventory was undefined" });
return;
}
if (inventory.GuildId) { if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId); const guild = await Guild.findOne({ _id: inventory.GuildId });
if (guild) { if (guild) {
// Handle guilds created before we added discriminators res.json({
if (guild.Name.indexOf("#") == -1) { _id: toOid(guild._id),
guild.Name = await createUniqueClanName(guild.Name); Name: guild.Name,
await guild.save(); Members: [
{
_id: { $oid: req.query.accountId },
Rank: 0,
Status: 0
} }
],
if (guild.CeremonyResetDate && Date.now() >= guild.CeremonyResetDate.getTime()) { Ranks: [
logger.debug(`ascension ceremony is over`); {
guild.CeremonyEndo = undefined; Name: "/Lotus/Language/Game/Rank_Creator",
guild.CeremonyContributors = undefined; Permissions: 16351
guild.CeremonyResetDate = undefined; },
await guild.save(); {
Name: "/Lotus/Language/Game/Rank_Warlord",
Permissions: 14303
},
{
Name: "/Lotus/Language/Game/Rank_General",
Permissions: 4318
},
{
Name: "/Lotus/Language/Game/Rank_Officer",
Permissions: 4314
},
{
Name: "/Lotus/Language/Game/Rank_Leader",
Permissions: 4106
},
{
Name: "/Lotus/Language/Game/Rank_Sage",
Permissions: 4304
},
{
Name: "/Lotus/Language/Game/Rank_Soldier",
Permissions: 4098
},
{
Name: "/Lotus/Language/Game/Rank_Initiate",
Permissions: 4096
},
{
Name: "/Lotus/Language/Game/Rank_Utility",
Permissions: 4096
} }
res.json(await getGuildClient(guild, accountId)); ],
Tier: 1
});
return; return;
} }
} }
res.end(); res.json({});
}; };
export { getGuildController };

View File

@ -6,28 +6,24 @@ import { getDojoClient } from "@/src/services/guildService";
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;
const guild = await Guild.findById(guildId); const guild = await Guild.findOne({ _id: guildId });
if (!guild) { if (!guild) {
res.status(404).end(); res.status(404).end();
return; return;
} }
// Populate dojo info if not present // Populate dojo info if not present
if (guild.DojoComponents.length == 0) { if (!guild.DojoComponents || guild.DojoComponents.length == 0) {
guild.DojoComponents.push({ guild.DojoComponents = [
{
_id: new Types.ObjectId(), _id: new Types.ObjectId(),
pf: "/Lotus/Levels/ClanDojo/DojoHall.level", pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
ppf: "", ppf: "",
CompletionTime: new Date(Date.now()), CompletionTime: new Date(Date.now())
DecoCapacity: 600 }
}); ];
await guild.save(); await guild.save();
} }
const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {}; res.json(getDojoClient(guild, 0));
res.json(await getDojoClient(guild, 0, payload.ComponentId));
}; };
interface IGetGuildDojoRequest {
ComponentId?: string;
}

View File

@ -1,60 +1,11 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { Guild } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMongoDate } from "@/src/types/commonTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getGuildLogController: RequestHandler = async (req, res) => { export const getGuildLogController: RequestHandler = (_req, res) => {
const accountId = await getAccountIdForRequest(req); res.json({
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId);
if (guild) {
const log: Record<string, IGuildLogEntryClient[]> = {
RoomChanges: [], RoomChanges: [],
TechChanges: [], TechChanges: [],
RosterActivity: [], RosterActivity: [],
StandingsUpdates: [], StandingsUpdates: [],
ClassChanges: [] ClassChanges: []
});
}; };
guild.RoomChanges?.forEach(entry => {
log.RoomChanges.push({
dateTime: toMongoDate(entry.dateTime ?? new Date()),
entryType: entry.entryType,
details: entry.details
});
});
guild.TechChanges?.forEach(entry => {
log.TechChanges.push({
dateTime: toMongoDate(entry.dateTime ?? new Date()),
entryType: entry.entryType,
details: entry.details
});
});
guild.RosterActivity?.forEach(entry => {
log.RosterActivity.push({
dateTime: toMongoDate(entry.dateTime),
entryType: entry.entryType,
details: entry.details
});
});
guild.ClassChanges?.forEach(entry => {
log.ClassChanges.push({
dateTime: toMongoDate(entry.dateTime),
entryType: entry.entryType,
details: entry.details
});
});
res.json(log);
return;
}
}
res.sendStatus(200);
};
interface IGuildLogEntryClient {
dateTime: IMongoDate;
entryType: number;
details: number | string;
}

View File

@ -1,20 +1,16 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getIgnoredUsersController: RequestHandler = async (req, res) => { const getIgnoredUsersController: RequestHandler = (_req, res) => {
const accountId = await getAccountIdForRequest(req); res.writeHead(200, {
const ignores = await Ignore.find({ ignorer: accountId }); "Content-Type": "text/html",
const ignoredUsers: IFriendInfo[] = []; "Content-Length": "3"
await parallelForeach(ignores, async ignore => {
const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
ignoredUsers.push({
_id: toOid(ignore.ignoree),
DisplayName: ignoreeAccount.DisplayName + ""
}); });
}); res.end(
res.json({ IgnoredUsers: ignoredUsers }); Buffer.from([
0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32,
0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d
])
);
}; };
export { getIgnoredUsersController };

View File

@ -1,19 +1,13 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => { const getNewRewardSeedController: RequestHandler = (_req, res) => {
const accountId = await getAccountIdForRequest(req); res.json({ rewardSeed: generateRewardSeed() });
const rewardSeed = generateRewardSeed();
await Inventory.updateOne(
{
accountOwnerId: accountId
},
{
RewardSeed: rewardSeed
}
);
res.json({ rewardSeed: rewardSeed });
}; };
function generateRewardSeed(): number {
const min = -Number.MAX_SAFE_INTEGER;
const max = Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export { getNewRewardSeedController };

View File

@ -2,24 +2,17 @@ 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 { 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 ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
@ -33,10 +26,7 @@ export const getShipController: RequestHandler = async (req, res) => {
Colors: personalRooms.ShipInteriorColors, Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments, ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem SkinFlavourItem: ship.SkinFlavourItem
}, }
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)
: undefined
}, },
Apartment: personalRooms.Apartment, Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop TailorShop: personalRooms.TailorShop

View File

@ -1,31 +1,77 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { crackRelic } from "@/src/helpers/relicHelper"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IVoidTearParticipantInfo } from "@/src/types/requestTypes"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getRandomWeightedReward2 } from "@/src/services/rngService";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus";
export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => { export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IVoidProjectionRewardRequest>(String(req.body)); const data = getJSONfromString<IVoidProjectionRewardRequest>(String(req.body));
if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
const inventory = await getInventory(accountId);
await crackRelic(inventory, data.ParticipantInfo);
await inventory.save();
}
const response: IVoidProjectionRewardResponse = { const response: IVoidProjectionRewardResponse = {
CurrentWave: data.CurrentWave, CurrentWave: data.CurrentWave,
ParticipantInfo: data.ParticipantInfo, ParticipantInfo: data.ParticipantInfo,
DifficultyTier: data.DifficultyTier DifficultyTier: data.DifficultyTier
}; };
if (data.ParticipantInfo.QualifiesForReward) {
const relic = ExportRelics[data.ParticipantInfo.VoidProjection];
const weights = refinementToWeights[relic.quality];
logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
const reward = getRandomWeightedReward2(
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights
)!;
logger.debug(`relic rolled`, reward);
response.ParticipantInfo.Reward = reward.type;
const inventory = await getInventory(accountId);
// Remove relic
addMiscItems(inventory, [
{
ItemType: data.ParticipantInfo.VoidProjection,
ItemCount: -1
}
]);
// Give reward
await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount);
await inventory.save();
}
res.json(response); res.json(response);
}; };
const refinementToWeights = {
VPQ_BRONZE: {
COMMON: 0.76,
UNCOMMON: 0.22,
RARE: 0.02,
LEGENDARY: 0
},
VPQ_SILVER: {
COMMON: 0.7,
UNCOMMON: 0.26,
RARE: 0.04,
LEGENDARY: 0
},
VPQ_GOLD: {
COMMON: 0.6,
UNCOMMON: 0.34,
RARE: 0.06,
LEGENDARY: 0
},
VPQ_PLATINUM: {
COMMON: 0.5,
UNCOMMON: 0.4,
RARE: 0.1,
LEGENDARY: 0
}
};
interface IVoidProjectionRewardRequest { interface IVoidProjectionRewardRequest {
CurrentWave: number; CurrentWave: number;
ParticipantInfo: IVoidTearParticipantInfo; ParticipantInfo: IParticipantInfo;
VoidTier: string; VoidTier: string;
DifficultyTier: number; DifficultyTier: number;
VoidProjectionRemovalHash: string; VoidProjectionRemovalHash: string;
@ -33,6 +79,20 @@ interface IVoidProjectionRewardRequest {
interface IVoidProjectionRewardResponse { interface IVoidProjectionRewardResponse {
CurrentWave: number; CurrentWave: number;
ParticipantInfo: IVoidTearParticipantInfo; ParticipantInfo: IParticipantInfo;
DifficultyTier: number; DifficultyTier: number;
} }
interface IParticipantInfo {
AccountId: string;
Name: string;
ChosenRewardOwner: string;
MissionHash: string;
VoidProjection: string;
Reward: string;
QualifiesForReward: boolean;
HaveRewardResponse: boolean;
RewardsMultiplier: number;
RewardProjection: string;
HardModeReward: ITypeCount;
}

View File

@ -1,92 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account } from "@/src/models/loginModel";
import { createMessage } from "@/src/services/inboxService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body));
if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) {
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
}
const account = await Account.findOne(
data.RecipientId ? { _id: data.RecipientId.$oid } : { DisplayName: data.Recipient }
);
if (!account) {
res.status(400).send("9").end();
return;
}
const inventory = await getInventory(account._id.toString(), "Suits Settings");
// Cannot gift items to players that have not completed the tutorial.
if (inventory.Suits.length == 0) {
res.status(400).send("14").end();
return;
}
// Cannot gift to players who have gifting disabled.
// TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") {
res.status(400).send("17").end();
return;
}
// TODO: Cannot gift items with mastery requirement to players who are too low level. (Code 2)
// 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)
const senderAccount = await getAccountForRequest(req);
const senderInventory = await getInventory(
senderAccount._id.toString(),
"PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining"
);
if (senderInventory.GiftsRemaining == 0) {
res.status(400).send("10").end();
return;
}
senderInventory.GiftsRemaining -= 1;
updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true);
await senderInventory.save();
const senderName = getSuffixedName(senderAccount);
await createMessage(account._id, [
{
sndr: senderName,
msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage",
arg: [
{
Key: "GIFTER_NAME",
Tag: senderName
},
{
Key: "GIFT_QUANTITY",
Tag: data.PurchaseParams.Quantity
}
],
sub: "/Lotus/Language/Menu/GiftReceivedSubject",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
gifts: [
{
GiftType: data.PurchaseParams.StoreItem
}
]
}
]);
res.end();
};
interface IGiftingRequest {
PurchaseParams: IPurchaseParams;
Message?: string;
Recipient?: string;
RecipientId?: IOid;
buildLabel: string;
}

View File

@ -1,26 +1,37 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards" // Not sure about hoverboards just coppied from modual crafting
];
interface IGildWeaponRequest { interface IGildWeaponRequest {
ItemName: string; ItemName: string;
Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint Recipe: string; // /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
PolarizeSlot?: number; PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity; PolarizeValue?: ArtifactPolarity;
ItemId: string; ItemId: string;
Category: TEquipmentKey; Category: WeaponTypeInternal | "Hoverboards";
} }
// In export there no recipes for gild action, so reputation and ressources only consumed visually
export const gildWeaponController: RequestHandler = async (req, res) => { export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IGildWeaponRequest>(String(req.body)); const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
data.ItemId = String(req.query.ItemId); data.ItemId = String(req.query.ItemId);
data.Category = req.query.Category as TEquipmentKey; if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
}
data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId); const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
@ -29,12 +40,9 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
} }
const weapon = inventory[data.Category][weaponIndex]; const weapon = inventory[data.Category][weaponIndex];
weapon.Features ??= 0; weapon.Features = EquipmentFeatures.GILDED; // maybe 9 idk if DOUBLE_CAPACITY is also given
weapon.Features |= EquipmentFeatures.GILDED;
if (data.Recipe != "webui") {
weapon.ItemName = data.ItemName; weapon.ItemName = data.ItemName;
weapon.XP = 0; weapon.XP = 0;
}
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
weapon.Polarity = [ weapon.Polarity = [
{ {
@ -44,32 +52,11 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
]; ];
} }
inventory[data.Category][weaponIndex] = weapon; inventory[data.Category][weaponIndex] = weapon;
const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const affiliationMods = [];
if (data.Recipe != "webui") {
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value;
affiliationMods.push({
Tag: recipe.syndicateStandingChange.tag,
Standing: recipe.syndicateStandingChange.value
});
}
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: {
AffiliationMods: affiliationMods [data.Category]: [weapon]
}
}); });
}; };

View File

@ -1,17 +1,38 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general"; import { isEmptyObject, parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { addKeyChainItems, getInventory } from "@/src/services/inventoryService";
import { giveKeyChainItem } from "@/src/services/questService"; import { IGroup } from "@/src/types/loginTypes";
import { IKeyChainRequest } from "@/src/types/requestTypes"; import { updateQuestStage } from "@/src/services/questService";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId); const accountId = parseString(req.query.accountId);
const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString()); const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString());
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo); const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo);
await inventory.save();
if (isEmptyObject(inventoryChanges)) {
throw new Error("inventory changes was empty after getting keychain items: should not happen");
}
// items were added: update quest stage's i (item was given)
updateQuestStage(inventory, keyChainInfo, { i: true });
await inventory.save();
res.send(inventoryChanges); res.send(inventoryChanges);
//TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory
/*
some items are added or removed (not sure) to the wishlist, in that case a
WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],
is added to the response, need to determine for which items this is the case and what purpose this has.
*/
//{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0}
//{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]}
}; };
export interface IKeyChainRequest {
KeyChain: string;
ChainStage: number;
Groups?: IGroup[];
}

View File

@ -1,15 +1,34 @@
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { IMessage } from "@/src/models/inboxModel";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getKeyChainMessage } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { giveKeyChainMessage } from "@/src/services/questService"; import { updateQuestStage } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest; const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest;
const keyChainMessage = getKeyChainMessage(keyChainInfo);
const message = {
sndr: keyChainMessage.sender,
msg: keyChainMessage.body,
sub: keyChainMessage.title,
att: keyChainMessage.attachments.length > 0 ? keyChainMessage.attachments : undefined,
countedAtt: keyChainMessage.countedAttachments.length > 0 ? keyChainMessage.countedAttachments : undefined,
icon: keyChainMessage.icon ?? "",
transmission: keyChainMessage.transmission ?? "",
highPriority: keyChainMessage.highPriority ?? false,
r: false
} satisfies IMessage;
await createMessage(accountId, [message]);
const inventory = await getInventory(accountId, "QuestKeys"); const inventory = await getInventory(accountId, "QuestKeys");
await giveKeyChainMessage(inventory, accountId, keyChainInfo); updateQuestStage(inventory, keyChainInfo, { m: true });
await inventory.save(); await inventory.save();
res.send(1); res.send(1);

View File

@ -16,15 +16,15 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
await inventory.save(); await inventory.save();
res.json(inventoryChanges); res.json(inventoryChanges.InventoryChanges);
//TODO: consider whishlist changes //TODO: consider whishlist changes
}; };
interface IQuestKeyRewardRequest { export interface IQuestKeyRewardRequest {
reward: IQuestKeyReward; reward: IQuestKeyReward;
} }
interface IQuestKeyReward { export interface IQuestKeyReward {
RewardType: string; RewardType: string;
CouponType: string; CouponType: string;
Icon: string; Icon: string;
@ -38,7 +38,7 @@ interface IQuestKeyReward {
Duration: number; Duration: number;
CouponSku: number; CouponSku: number;
Syndicate: string; Syndicate: string;
//Milestones: any[]; Milestones: any[];
ChooseSetIndex: number; ChooseSetIndex: number;
NewSystemReward: boolean; NewSystemReward: boolean;
_id: IOid; _id: IOid;

View File

@ -1,20 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations");
const data = getJSONfromString<IGiveShipDecoAndLoreFragmentRequest>(String(req.body));
addLoreFragmentScans(inventory, data.LoreFragmentScans);
addShipDecorations(inventory, data.ShipDecorations);
await inventory.save();
res.end();
};
interface IGiveShipDecoAndLoreFragmentRequest {
LoreFragmentScans: ILoreFragmentScan[];
ShipDecorations: ITypeCount[];
}

View File

@ -1,16 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addStartingGear, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const giveStartingGearController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const startingGear = getJSONfromString<TPartialStartingGear>(String(req.body));
const inventory = await getInventory(accountId);
const inventoryChanges = await addStartingGear(inventory, startingGear);
await inventory.save();
res.send(inventoryChanges);
};

View File

@ -1,142 +1,46 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { import { getGuildForRequestEx } from "@/src/services/guildService";
addGuildMemberMiscItemContribution,
getGuildForRequestEx,
getGuildVault,
hasAccessToDojo,
hasGuildPermission,
processFundedGuildTechProject,
processGuildTechProjectContributionsUpdate,
removePigmentsFromGuildMembers,
scaleRequiredCount,
setGuildTechLogState
} from "@/src/services/guildService";
import { ExportDojoRecipes } 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 { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
addCrewShipWeaponSkin, import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
addEquipment,
addItem,
addMiscItems,
addRecipes,
combineInventoryChanges,
getInventory,
occupySlot,
updateCurrency
} from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
import { GuildMember } from "@/src/models/guildModel";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const guildTechController: RequestHandler = async (req, res) => { export const guildTechController: 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 guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest; const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") { const action = data.Action.split(",")[0];
let needSave = false; if (action == "Sync") {
const techProjects: ITechProjectClient[] = []; res.json({
const guild = await getGuildForRequestEx(req, inventory); TechProjects: guild.toJSON().TechProjects
if (guild.TechProjects) { });
for (const project of guild.TechProjects) { } else if (action == "Start") {
const techProject: ITechProjectClient = { const recipe = ExportDojoRecipes.research[data.RecipeType!];
ItemType: project.ItemType,
ReqCredits: project.ReqCredits,
ReqItems: project.ReqItems,
State: project.State
};
if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate);
if (Date.now() >= project.CompletionDate.getTime()) {
needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate);
}
}
techProjects.push(techProject);
}
}
if (needSave) {
await guild.save();
}
res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") {
if (data.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= []; guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
const techProject =
guild.TechProjects[
guild.TechProjects.push({ guild.TechProjects.push({
ItemType: data.RecipeType, ItemType: data.RecipeType!,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), ReqCredits: scaleRequiredCount(recipe.price),
ReqItems: recipe.ingredients.map(x => ({ ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType, ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) ItemCount: scaleRequiredCount(x.ItemCount)
})), })),
State: 0 State: 0
}) - 1 });
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
}
}
} }
await guild.save(); await guild.save();
res.end(); res.end();
} else { } else if (action == "Contribute") {
const recipe = ExportDojoRecipes.research[data.RecipeType]; const contributions = data as IGuildTechContributeFields;
if (data.TechProductCategory) { const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if ( if (contributions.RegularCredits > techProject.ReqCredits) {
data.TechProductCategory != "CrewShipWeapons" && contributions.RegularCredits = techProject.ReqCredits;
data.TechProductCategory != "CrewShipWeaponSkins"
) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
} }
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) { techProject.ReqCredits -= contributions.RegularCredits;
throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
);
}
}
const techProject =
inventory.PersonalTechProjects[
inventory.PersonalTechProjects.push({
State: 0,
ReqCredits: recipe.price,
ItemType: data.RecipeType,
ProductCategory: data.TechProductCategory,
CategoryItemId: data.CategoryItemId,
ReqItems: recipe.ingredients
}) - 1
];
await inventory.save();
res.json({
isPersonal: true,
action: "Start",
personalTech: techProject.toJSON()
});
}
} else if (data.Action == "Contribute") {
if ((req.query.guildId as string) == "000000000000000000000000") {
const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
techProject.ReqCredits -= data.RegularCredits;
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
const miscItemChanges = []; const miscItemChanges = [];
for (const miscItem of data.MiscItems) { for (const miscItem of contributions.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -150,108 +54,27 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges; const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, contributions.RegularCredits, false),
techProject.HasContributions = true; MiscItems: miscItemChanges
};
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
// This research is now fully funded.
techProject.State = 1; techProject.State = 1;
const recipe = ExportDojoRecipes.research[techProject.ItemType]; const recipe = ExportDojoRecipes.research[data.RecipeType!];
techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000); techProject.CompletionDate = new Date(new Date().getTime() + recipe.time * 1000);
} }
await guild.save();
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges
PersonalResearch: { $oid: data.ResearchId },
PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
}); });
} else { } else if (action == "Buy") {
if (!hasAccessToDojo(inventory)) { const purchase = data as IGuildTechBuyFields;
res.status(400).send("-1").end();
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
if (data.VaultCredits) {
if (data.VaultCredits > techProject.ReqCredits) {
data.VaultCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= data.VaultCredits;
guild.VaultRegularCredits! -= data.VaultCredits;
}
if (data.RegularCredits > techProject.ReqCredits) {
data.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= data.RegularCredits;
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += data.RegularCredits;
if (data.VaultMiscItems.length) {
for (const miscItem of data.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
}
}
}
const miscItemChanges = [];
for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
addGuildMemberMiscItemContribution(guildMember, miscItem);
}
}
addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now.
await processGuildTechProjectContributionsUpdate(guild, techProject);
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
});
}
} else if (data.Action.split(",")[0] == "Buy") {
const purchase = data as IGuildTechBuyRequest;
if (purchase.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (
!hasAccessToDojo(inventory) ||
!(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))
) {
res.status(400).send("-1").end();
return;
}
const quantity = parseInt(data.Action.split(",")[1]); const quantity = parseInt(data.Action.split(",")[1]);
const inventory = await getInventory(accountId);
const recipeChanges = [ const recipeChanges = [
{ {
ItemType: purchase.RecipeType, ItemType: purchase.RecipeType,
@ -273,135 +96,24 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
}); });
} else { } else {
const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!); throw new Error(`unknown guildTech action: ${data.Action}`);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
}
} else if (data.Action == "Fabricate") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.fabrications[data.RecipeType];
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false);
inventoryChanges.MiscItems = recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: x.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType));
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
project.State = -2;
guild.ActiveDojoColorResearch = "";
await guild.save();
await removePigmentsFromGuildMembers(guild._id);
res.end();
} else if (data.Action == "Unpause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
project.State = 0;
guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save();
res.end();
} else if (data.Action == "Cancel" && data.CategoryItemId) {
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
x.CategoryItemId?.equals(data.CategoryItemId)
);
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
const contributedCredits = meta.price - personalTechProject.ReqCredits;
const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false);
inventoryChanges.MiscItems = [];
for (const ingredient of meta.ingredients) {
const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
if (reqItem) {
const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
inventoryChanges.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: contributedItems
});
}
}
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
action: "Cancel",
isPersonal: true,
inventoryChanges: inventoryChanges,
personalTech: {
ItemId: toOid(personalTechProject._id)
}
});
} else if (data.Action == "Rush" && data.CategoryItemId) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, 20, true),
...claimSalvagedComponent(inventory, data.CategoryItemId)
};
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else if (data.Action == "InstantFinish") {
if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!);
inventoryChanges.MiscItems = [
{
ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem",
ItemCount: -1
}
];
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled guildTech request`);
} }
}; };
type TGuildTechRequest = type TGuildTechRequest = {
| { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" }
| IGuildTechBasicRequest
| IGuildTechContributeRequest;
interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish";
Mode: "Guild" | "Personal";
RecipeType: string;
TechProductCategory?: string;
CategoryItemId?: string;
}
interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
Action: string; Action: string;
} & Partial<IGuildTechStartFields> &
Partial<IGuildTechContributeFields>;
interface IGuildTechStartFields {
Mode: "Guild";
RecipeType: string;
} }
interface IGuildTechContributeRequest { type IGuildTechBuyFields = IGuildTechStartFields;
Action: "Contribute";
ResearchId: string; interface IGuildTechContributeFields {
ResearchId: "";
RecipeType: string; RecipeType: string;
RegularCredits: number; RegularCredits: number;
MiscItems: IMiscItem[]; MiscItems: IMiscItem[];
@ -409,49 +121,7 @@ interface IGuildTechContributeRequest {
VaultMiscItems: IMiscItem[]; VaultMiscItems: IMiscItem[];
} }
const getSalvageCategory = ( const scaleRequiredCount = (count: number): number => {
category: "CrewShipWeapons" | "CrewShipWeaponSkins" // The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans.
): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => { return Math.max(1, Math.trunc(count / 100));
return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
};
const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => {
// delete personal tech project
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins";
return finishComponentRepair(inventory, category, itemId);
};
const finishComponentRepair = (
inventory: TInventoryDatabaseDocument,
category: "CrewShipWeapons" | "CrewShipWeaponSkins",
itemId: string
): IInventoryChanges => {
const salvageCategory = getSalvageCategory(category);
// find salved part & delete it
const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId));
const salvageItem = inventory[salvageCategory][salvageIndex];
inventory[salvageCategory].splice(salvageIndex, 1);
// add final item
const inventoryChanges = {
...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeFingerprint: salvageItem.UpgradeFingerprint
})),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
};
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: itemId }
}
];
return inventoryChanges;
}; };

View File

@ -1,45 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addBooster, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomInt } from "@/src/services/rngService";
import { RequestHandler } from "express";
import { ExportBoosters } from "warframe-public-export-plus";
export const hubBlessingController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IHubBlessingRequest>(String(req.body));
const boosterType = ExportBoosters[data.booster].typeName;
if (req.query.mode == "send") {
const inventory = await getInventory(accountId, "BlessingCooldown Boosters");
inventory.BlessingCooldown = new Date(Date.now() + 86400000);
addBooster(boosterType, 3 * 3600, inventory);
await inventory.save();
let token = "";
for (let i = 0; i != 32; ++i) {
token += getRandomInt(0, 15).toString(16);
}
res.json({
BlessingCooldown: inventory.BlessingCooldown,
SendTime: Math.trunc(Date.now() / 1000).toString(),
Token: token
});
} else {
const inventory = await getInventory(accountId, "Boosters");
addBooster(boosterType, 3 * 3600, inventory);
await inventory.save();
res.json({
BoosterType: data.booster,
Sender: data.senderId
});
}
};
interface IHubBlessingRequest {
booster: string;
senderId?: string; // mode=request
sendTime?: string; // mode=request
token?: string; // mode=request
}

View File

@ -1,25 +1,21 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Inbox } from "@/src/models/inboxModel"; import { Inbox } from "@/src/models/inboxModel";
import { import {
createMessage,
createNewEventMessages, createNewEventMessages,
deleteAllMessagesRead, deleteAllMessagesRead,
deleteMessageRead, deleteMessageRead,
getAllMessagesSorted, getAllMessagesSorted,
getMessage getMessage
} from "@/src/services/inboxService"; } from "@/src/services/inboxService";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { addItems, getInventory } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFlavour, ExportGear } from "warframe-public-export-plus"; import { ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
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;
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const accountId = account._id.toString();
if (deleteId) { if (deleteId) {
if (deleteId === "DeleteAllRead") { if (deleteId === "DeleteAllRead") {
@ -33,12 +29,12 @@ export const inboxController: RequestHandler = async (req, res) => {
} else if (messageId) { } else if (messageId) {
const message = await getMessage(messageId as string); const message = await getMessage(messageId as string);
message.r = true; message.r = true;
const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt;
if (!attachmentItems && !attachmentCountedItems) {
await message.save(); await message.save();
const attachmentItems = message.attVisualOnly ? undefined : message.att;
const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
res.status(200).end(); res.status(200).end();
return; return;
} }
@ -49,7 +45,7 @@ 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: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1 ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})), })),
inventoryChanges inventoryChanges
@ -58,43 +54,9 @@ export const inboxController: RequestHandler = async (req, res) => {
if (attachmentCountedItems) { if (attachmentCountedItems) {
await addItems(inventory, attachmentCountedItems, inventoryChanges); await addItems(inventory, attachmentCountedItems, inventoryChanges);
} }
if (message.gifts) {
const sender = await getAccountFromSuffixedName(message.sndr);
const recipientName = getSuffixedName(account);
const giftQuantity = message.arg!.find(x => x.Key == "GIFT_QUANTITY")!.Tag as number;
for (const gift of message.gifts) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges
);
if (sender) {
await createMessage(sender._id, [
{
sndr: recipientName,
msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody",
arg: [
{
Key: "RECIPIENT_NAME",
Tag: recipientName
},
{
Key: "GIFT_TYPE",
Tag: gift.GiftType
},
{
Key: "GIFT_QUANTITY",
Tag: giftQuantity
}
],
sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
icon: ExportFlavour[inventory.ActiveAvatarImageType].icon,
highPriority: true
}
]);
}
}
}
await inventory.save(); await inventory.save();
await message.save();
res.json({ InventoryChanges: inventoryChanges }); res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) { } else if (latestClientMessageId) {
await createNewEventMessages(req); await createNewEventMessages(req);

View File

@ -1,26 +1,21 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } 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 } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { import {
IConsumedSuit, IConsumedSuit,
IHelminthFoodRecord, IHelminthFoodRecord,
IInventoryClient, IInfestedFoundryDatabase,
IMiscItem, IMiscItem,
InventorySlot ITypeCount
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc } from "warframe-public-export-plus"; import { ExportMisc, ExportRecipes } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { toMongoDate } 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 {
addInfestedFoundryXP,
applyCheatsToInfestedFoundry,
handleSubsumeCompletion
} from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -29,7 +24,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard installation // shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
} }
@ -57,40 +52,35 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard removal // shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body)); const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = [];
if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
// refund shard // refund shard
const shard = Object.entries(colorToShard).find( const shard = Object.entries(colorToShard).find(
([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color ([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color
)![1]; )![1];
miscItemChanges.push({ const miscItemChanges = [
{
ItemType: shard, ItemType: shard,
ItemCount: 1 ItemCount: 1
});
addMiscItems(inventory, miscItemChanges);
} }
];
addMiscItems(inventory, miscItemChanges);
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {}; suit.ArchonCrystalUpgrades![request.Slot] = {};
if (!config.infiniteHelminthMaterials) {
// remove bile // remove bile
const bile = inventory.InfestedFoundry!.Resources!.find( const bile = inventory.InfestedFoundry!.Resources!.find(
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile" x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
)!; )!;
bile.Count -= 300; bile.Count -= 300;
}
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
MiscItems: miscItemChanges, MiscItems: miscItemChanges,
InfestedFoundry: infestedFoundry InfestedFoundry: inventory.toJSON().InfestedFoundry
} }
}); });
break; break;
@ -115,12 +105,6 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "c": { case "c": {
// consume items // consume items
if (config.infiniteHelminthMaterials) {
res.status(400).end();
return;
}
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body)); const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
@ -129,7 +113,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
let totalPercentagePointsGained = 0; let totalPercentagePointsGained = 0;
const currentUnixSeconds = Math.trunc(Date.now() / 1000); const currentUnixSeconds = Math.trunc(new Date().getTime() / 1000);
for (const contribution of request.ResourceContributions) { for (const contribution of request.ResourceContributions) {
const snack = ExportMisc.helminthSnacks[contribution.ItemType]; const snack = ExportMisc.helminthSnacks[contribution.ItemType];
@ -226,11 +210,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
inventory.InfestedFoundry.InvigorationsApplied = 0; inventory.InfestedFoundry.InvigorationsApplied = 0;
} }
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
InfestedFoundry: infestedFoundry InfestedFoundry: inventory.toJSON().InfestedFoundry
} }
}); });
break; break;
@ -241,18 +223,16 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body)); const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const recipe = getRecipe(request.Recipe)!; const recipe = getRecipe(request.Recipe)!;
if (!config.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) { for (const ingredient of recipe.secretIngredients!) {
const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType); const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType);
if (resource) { if (resource) {
resource.Count -= ingredient.ItemCount; resource.Count -= ingredient.ItemCount;
} }
} }
}
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
inventory.Suits.pull(suit); inventory.Suits.pull(suit);
const consumedSuit: IConsumedSuit = { s: suit.ItemType }; const consumedSuit: IConsumedSuit = { s: suit.ItemType };
if (suit.Configs[0] && suit.Configs[0].pricol) { if (suit.Configs && suit.Configs[0] && suit.Configs[0].pricol) {
consumedSuit.c = suit.Configs[0].pricol; consumedSuit.c = suit.Configs[0].pricol;
} }
if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) { if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) {
@ -261,13 +241,12 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
inventory.InfestedFoundry!.ConsumedSuits ??= []; inventory.InfestedFoundry!.ConsumedSuits ??= [];
inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit); inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit);
inventory.InfestedFoundry!.LastConsumedSuit = suit; inventory.InfestedFoundry!.LastConsumedSuit = suit;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(Date.now() + 24 * 60 * 60 * 1000); inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(
new Date().getTime() + 24 * 60 * 60 * 1000
);
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00);
addRecipes(inventory, recipeChanges); addRecipes(inventory, recipeChanges);
freeUpSlot(inventory, InventorySlot.SUITS);
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
Recipes: recipeChanges, Recipes: recipeChanges,
@ -281,7 +260,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
platinum: 0, platinum: 0,
Slots: 1 Slots: 1
}, },
InfestedFoundry: infestedFoundry InfestedFoundry: inventory.toJSON().InfestedFoundry
} }
}); });
break; break;
@ -293,13 +272,11 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
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();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
...currencyChanges, ...currencyChanges,
Recipes: recipeChanges, Recipes: recipeChanges,
InfestedFoundry: infestedFoundry InfestedFoundry: inventory.toJSON().InfestedFoundry
} }
}); });
break; break;
@ -309,23 +286,19 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body)); const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(accountId); 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(new Date().getTime() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.OffensiveUpgrade = request.OffensiveUpgradeType;
suit.DefensiveUpgrade = request.DefensiveUpgradeType; suit.DefensiveUpgrade = request.DefensiveUpgradeType;
suit.UpgradesExpiry = upgradesExpiry; suit.UpgradesExpiry = upgradesExpiry;
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00);
addRecipes(inventory, recipeChanges); addRecipes(inventory, recipeChanges);
if (!config.infiniteHelminthMaterials) {
for (let i = 0; i != request.ResourceTypes.length; ++i) { for (let i = 0; i != request.ResourceTypes.length; ++i) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -= inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -=
request.ResourceCosts[i]; request.ResourceCosts[i];
} }
}
inventory.InfestedFoundry!.InvigorationsApplied ??= 0; inventory.InfestedFoundry!.InvigorationsApplied ??= 0;
inventory.InfestedFoundry!.InvigorationsApplied += 1; inventory.InfestedFoundry!.InvigorationsApplied += 1;
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
SuitId: request.SuitId, SuitId: request.SuitId,
OffensiveUpgrade: request.OffensiveUpgradeType, OffensiveUpgrade: request.OffensiveUpgradeType,
@ -333,7 +306,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
UpgradesExpiry: toMongoDate(upgradesExpiry), UpgradesExpiry: toMongoDate(upgradesExpiry),
InventoryChanges: { InventoryChanges: {
Recipes: recipeChanges, Recipes: recipeChanges,
InfestedFoundry: infestedFoundry InfestedFoundry: inventory.toJSON().InfestedFoundry
} }
}); });
break; break;
@ -356,7 +329,6 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
default: default:
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`); throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
} }
}; };
@ -384,11 +356,103 @@ interface IHelminthFeedRequest {
}[]; }[];
} }
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
const recipeChanges: ITypeCount[] = [];
infestedFoundry.XP ??= 0;
const prevXP = infestedFoundry.XP;
infestedFoundry.XP += delta;
if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 3;
}
if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
ItemCount: 1
});
}
if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
}
if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 10;
}
if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
ItemCount: 1
});
}
if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
}
if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 20;
}
if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
}
if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
infestedFoundry.Slots = 1;
}
if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
ItemCount: 1
});
}
if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
ItemCount: 1
});
}
if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
ItemCount: 1
});
}
if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
ItemCount: 1
});
}
if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
ItemCount: 1
});
}
return recipeChanges;
};
interface IHelminthSubsumeRequest { interface IHelminthSubsumeRequest {
SuitId: IOid; SuitId: IOid;
Recipe: string; Recipe: string;
} }
export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => {
const [recipeType] = Object.entries(ExportRecipes).find(
([_recipeType, recipe]) =>
recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" &&
recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType
)!;
inventory.InfestedFoundry!.LastConsumedSuit = undefined;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined;
const recipeChanges: ITypeCount[] = [
{
ItemType: recipeType,
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
return recipeChanges;
};
interface IHelminthOfferingsUpdate { interface IHelminthOfferingsUpdate {
OfferingsIndex: number; OfferingsIndex: number;
SuitTypes: string[]; SuitTypes: string[];

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountForRequest } 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";
@ -13,22 +13,13 @@ import {
ExportResources, ExportResources,
ExportVirtuals ExportVirtuals
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { handleSubsumeCompletion } from "./infestedFoundryController";
import { import { allDailyAffiliationKeys } from "@/src/services/inventoryService";
addMiscItems,
allDailyAffiliationKeys,
cleanupInventory,
createLibraryDailyTask,
generateRewardSeed
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const account = await getAccountForRequest(request);
const inventory = await Inventory.findOne({ accountOwnerId: accountId }); const inventory = await Inventory.findOne({ accountOwnerId: account._id.toString() });
if (!inventory) { if (!inventory) {
response.status(400).json({ error: "inventory was undefined" }); response.status(400).json({ error: "inventory was undefined" });
@ -36,60 +27,16 @@ export const inventoryController: RequestHandler = async (request, response) =>
} }
// Handle daily reset // Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { const today: number = Math.trunc(new Date().getTime() / 86400000);
if (account.LastLoginDay != today) {
account.LastLoginDay = today;
await account.save();
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500; inventory[key] = 16000 + inventory.PlayerLevel * 500;
} }
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); await inventory.save();
inventory.TradesRemaining = inventory.PlayerLevel;
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
if (inventory.NextRefill) {
if (config.noArgonCrystalDecay) {
inventory.FoundToday = undefined;
} else {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const today = Math.trunc(Date.now() / 86400000);
const daysPassed = today - lastLoginDay;
for (let i = 0; i != daysPassed; ++i) {
const numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0;
if (numArgonCrystals == 0) {
break;
}
const numStableArgonCrystals = Math.min(
numArgonCrystals,
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0
);
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
numArgonCrystals,
numStableArgonCrystals,
numDecayingArgonCrystals,
numDecayingArgonCrystalsToRemove
});
// Remove half of owned decaying argon crystals
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal",
ItemCount: numDecayingArgonCrystalsToRemove * -1
}
]);
// All stable argon crystals are now decaying
inventory.FoundToday = undefined;
}
}
}
cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
//await inventory.save();
} }
if ( if (
@ -98,19 +45,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) { ) {
handleSubsumeCompletion(inventory); handleSubsumeCompletion(inventory);
//await inventory.save();
}
if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save(); await inventory.save();
}
response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query)); response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
}; };
@ -131,7 +67,7 @@ export const getInventoryResponse = async (
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
} }
if (config.infinitePlatinum) { if (config.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 0; inventoryResponse.PremiumCreditsFree = 999999999;
inventoryResponse.PremiumCredits = 999999999; inventoryResponse.PremiumCredits = 999999999;
} }
if (config.infiniteEndo) { if (config.infiniteEndo) {
@ -169,7 +105,7 @@ export const getInventoryResponse = async (
inventoryResponse.ShipDecorations = []; inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) { for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") { if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 }); inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
} }
} }
} }
@ -222,8 +158,7 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) { if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = []; const Polarity: IPolarity[] = [];
// 12 is needed for necramechs. 15 is needed for plexus/crewshipharness. for (let i = 0; i != 12; ++i) {
for (let i = 0; i != 15; ++i) {
Polarity.push({ Polarity.push({
Slot: i, Slot: i,
Value: ArtifactPolarity.Any Value: ArtifactPolarity.Any
@ -272,30 +207,24 @@ export const getInventoryResponse = async (
} }
if (config.noDailyStandingLimits) { if (config.noDailyStandingLimits) {
const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500);
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventoryResponse[key] = spoofedDailyAffiliation; inventoryResponse[key] = 999_999;
} }
} }
if (config.noDailyFocusLimit) { // Fix for #380
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000); inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } };
}
if (inventoryResponse.InfestedFoundry) { // This determines if the "void fissures" tab is shown in navigation.
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); inventoryResponse.HasOwnedVoidProjectionsPreviously = true;
}
// 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
inventoryResponse.HWIDProtectEnabled = true;
return inventoryResponse; return inventoryResponse;
}; };
const addString = (arr: string[], str: string): void => { export const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) { if (!arr.find(x => x == str)) {
arr.push(str); arr.push(str);
} }
@ -322,6 +251,15 @@ const resourceGetParent = (resourceName: string): string | undefined => {
if (resourceName in ExportResources) { if (resourceName in ExportResources) {
return ExportResources[resourceName].parentName; return ExportResources[resourceName].parentName;
} }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName; return ExportVirtuals[resourceName]?.parentName;
}; };
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
const catBreadHash = (name: string): number => {
let hash = 2166136261;
for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
hash = (hash * 16777619) & 0x7fffffff;
}
return hash;
};

View File

@ -3,7 +3,6 @@ import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { updateSlots } from "@/src/services/inventoryService"; import { updateSlots } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
/* /*
loadout slots are additionally purchased slots only loadout slots are additionally purchased slots only
@ -21,20 +20,14 @@ import { logger } from "@/src/utils/logger";
export const inventorySlotsController: RequestHandler = async (req, res) => { export const inventorySlotsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const body = JSON.parse(req.body as string) as IInventorySlotsRequest; //const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) { //TODO: check which slot was purchased because pvpBonus is also possible
logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`);
}
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 20, true); const currencyChanges = updateCurrency(inventory, 20, true);
updateSlots(inventory, body.Bin, 1, 1); updateSlots(inventory, InventorySlot.PVE_LOADOUTS, 1, 1);
await inventory.save(); await inventory.save();
res.json({ InventoryChanges: currencyChanges }); res.json({ InventoryChanges: currencyChanges });
}; };
interface IInventorySlotsRequest {
Bin: InventorySlot;
}

View File

@ -2,13 +2,12 @@ import { RequestHandler } from "express";
import { getSessionByID } from "@/src/managers/sessionManager"; import { getSessionByID } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
export const joinSessionController: RequestHandler = (req, res) => { const joinSessionController: RequestHandler = (_req, res) => {
const reqBody = JSON.parse(String(req.body)) as IJoinSessionRequest; const reqBody = JSON.parse(String(_req.body));
logger.debug(`JoinSession Request`, { reqBody }); logger.debug(`JoinSession Request`, { reqBody });
const session = getSessionByID(reqBody.sessionIds[0]); const req = JSON.parse(String(_req.body));
const session = getSessionByID(req.sessionIds[0] as string);
res.json({ rewardSeed: session?.rewardSeed, sessionId: { $oid: session?.sessionId } }); res.json({ rewardSeed: session?.rewardSeed, sessionId: { $oid: session?.sessionId } });
}; };
interface IJoinSessionRequest { export { joinSessionController };
sessionIds: string[];
}

View File

@ -6,6 +6,7 @@ import { buildConfig } from "@/src/services/buildConfigService";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } 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 { DTLS, groups, HUB, platformCDNs } from "@/static/fixed_responses/login_static";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
export const loginController: RequestHandler = async (request, response) => { export const loginController: RequestHandler = async (request, response) => {
@ -19,16 +20,10 @@ export const loginController: RequestHandler = async (request, response) => {
? request.query.buildLabel.split(" ").join("+") ? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel; : buildConfig.buildLabel;
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress; if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
if (
!account &&
((config.autoCreateAccount && loginRequest.ClientType != "webui") ||
loginRequest.ClientType == "webui-register")
) {
try { try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja"; let name = nameFromEmail;
if (await isNameTaken(name)) { if (await isNameTaken(name)) {
let suffix = 0; let suffix = 0;
do { do {
@ -41,15 +36,16 @@ export const loginController: RequestHandler = async (request, response) => {
password: loginRequest.password, password: loginRequest.password,
DisplayName: name, DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(), CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true, CrossPlatformAllowed: true,
ForceLogoutVersion: 0, ForceLogoutVersion: 0,
ConsentNeeded: false, ConsentNeeded: false,
TrackedSettings: [], TrackedSettings: [],
Nonce: nonce Nonce: nonce,
LatestEventMessageDate: new Date(0)
}); });
logger.debug("created new account"); logger.debug("created new account");
response.json(createLoginResponse(myAddress, newAccount, buildLabel)); response.json(createLoginResponse(newAccount, buildLabel));
return; return;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -58,42 +54,24 @@ export const loginController: RequestHandler = async (request, response) => {
} }
} }
if (!account) { //email not found or incorrect password
response.status(400).json({ error: "unknown user" }); if (!account || !isCorrectPassword(loginRequest.password, account.password)) {
return;
}
if (loginRequest.ClientType == "webui-register") {
response.status(400).json({ error: "account already exists" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" }); response.status(400).json({ error: "incorrect login data" });
return; return;
} }
if (loginRequest.ClientType == "webui") { if (account.Nonce == 0 || loginRequest.ClientType != "webui") {
if (!account.Nonce) {
account.ClientType = "webui";
account.Nonce = nonce; account.Nonce = nonce;
} }
} else { if (loginRequest.ClientType != "webui") {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
response.status(400).json({ error: "nonce still set" });
return;
}
account.ClientType = loginRequest.ClientType;
account.Nonce = nonce;
account.CountryCode = loginRequest.lang.toUpperCase(); account.CountryCode = loginRequest.lang.toUpperCase();
} }
await account.save(); await account.save();
response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel)); response.json(createLoginResponse(account.toJSON(), buildLabel));
}; };
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
return { return {
id: account.id, id: account.id,
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
@ -106,12 +84,12 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
ConsentNeeded: account.ConsentNeeded, ConsentNeeded: account.ConsentNeeded,
TrackedSettings: account.TrackedSettings, TrackedSettings: account.TrackedSettings,
Nonce: account.Nonce, Nonce: account.Nonce,
Groups: [], Groups: groups,
IRC: config.myIrcAddresses ?? [myAddress], platformCDNs: platformCDNs,
platformCDNs: [`https://${myAddress}/`], NRS: [config.myAddress],
HUB: `https://${myAddress}/api/`, DTLS: DTLS,
NRS: config.NRS, IRC: config.myIrcAddresses ?? [config.myAddress],
DTLS: 99, HUB: HUB,
BuildLabel: buildLabel, BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId MatchmakingBuildId: buildConfig.matchmakingBuildId
}; };

View File

@ -1,55 +1,8 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import loginRewards from "@/static/fixed_responses/loginRewards.json";
import {
claimLoginReward,
getRandomLoginRewards,
ILoginRewardsReponse,
isLoginRewardAChoice,
setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService";
import { getInventory } from "@/src/services/inventoryService";
export const loginRewardsController: RequestHandler = async (req, res) => { const loginRewardsController: RequestHandler = (_req, res) => {
const account = await getAccountForRequest(req); res.json(loginRewards);
const today = Math.trunc(Date.now() / 86400000) * 86400;
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50;
if (today == account.LastLoginRewardDate) {
res.json({
DailyTributeInfo: {
IsMilestoneDay: isMilestoneDay,
IsChooseRewardSet: isLoginRewardAChoice(account),
LoginDays: account.LoginDays,
NextMilestoneReward: "",
NextMilestoneDay: nextMilestoneDay
}
} satisfies ILoginRewardsReponse);
return;
}
const inventory = await getInventory(account._id.toString());
const randomRewards = getRandomLoginRewards(account, inventory);
const response: ILoginRewardsReponse = {
DailyTributeInfo: {
Rewards: randomRewards,
IsMilestoneDay: isMilestoneDay,
IsChooseRewardSet: randomRewards.length != 1,
LoginDays: account.LoginDays,
NextMilestoneReward: "",
NextMilestoneDay: nextMilestoneDay,
HasChosenReward: false
},
LastLoginRewardDate: today
}; };
if (!isMilestoneDay && randomRewards.length == 1) {
response.DailyTributeInfo.HasChosenReward = true;
response.DailyTributeInfo.ChosenReward = randomRewards[0];
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
await inventory.save();
setAccountGotLoginRewardToday(account); export { loginRewardsController };
await account.save();
}
res.json(response);
};

View File

@ -1,65 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import {
claimLoginReward,
getRandomLoginRewards,
setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService";
import { getAccountForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const loginRewardsSelectionController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString());
const body = JSON.parse(String(req.body)) as ILoginRewardsSelectionRequest;
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
if (body.IsMilestoneReward != isMilestoneDay) {
logger.warn(`Client disagrees on login milestone (got ${body.IsMilestoneReward}, expected ${isMilestoneDay})`);
}
let chosenReward;
let inventoryChanges: IInventoryChanges;
if (body.IsMilestoneReward) {
chosenReward = {
RewardType: "RT_STORE_ITEM",
StoreItemType: body.ChosenReward
};
inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
if (!evergreenRewards.find(x => x == body.ChosenReward)) {
inventory.LoginMilestoneRewards.push(body.ChosenReward);
}
} else {
const randomRewards = getRandomLoginRewards(account, inventory);
chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
inventoryChanges = await claimLoginReward(inventory, chosenReward);
}
await inventory.save();
setAccountGotLoginRewardToday(account);
await account.save();
res.json({
DailyTributeInfo: {
NewInventory: inventoryChanges,
ChosenReward: chosenReward
}
});
};
interface ILoginRewardsSelectionRequest {
ChosenReward: string;
IsMilestoneReward: boolean;
}
const evergreenRewards = [
"/Lotus/Types/StoreItems/Packages/EvergreenTripleForma",
"/Lotus/Types/StoreItems/Packages/EvergreenTripleRifleRiven",
"/Lotus/Types/StoreItems/Packages/EvergreenTripleMeleeRiven",
"/Lotus/Types/StoreItems/Packages/EvergreenTripleSecondaryRiven",
"/Lotus/Types/StoreItems/Packages/EvergreenWeaponSlots",
"/Lotus/Types/StoreItems/Packages/EvergreenKuva",
"/Lotus/Types/StoreItems/Packages/EvergreenBoosters",
"/Lotus/Types/StoreItems/Packages/EvergreenEndo",
"/Lotus/Types/StoreItems/Packages/EvergreenExilus"
];

View File

@ -1,28 +1,19 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
export const logoutController: RequestHandler = async (req, res) => { const logoutController: RequestHandler = async (req, res) => {
if (!req.query.accountId) { const accountId = await getAccountIdForRequest(req);
throw new Error("Request is missing accountId parameter"); const account = await Account.findOne({ _id: accountId });
if (account) {
account.Nonce = 0;
await account.save();
} }
const nonce: number = parseInt(req.query.nonce as string);
if (!nonce) {
throw new Error("Request is missing nonce parameter");
}
await Account.updateOne(
{
_id: req.query.accountId,
Nonce: nonce
},
{
Nonce: 0
}
);
res.writeHead(200, { res.writeHead(200, {
"Content-Type": "text/html", "Content-Type": "text/html",
"Content-Length": 1 "Content-Length": 1
}); });
res.end("1"); res.end("1");
}; };
export { logoutController };

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 maturePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets");
const data = getJSONfromString<IMaturePetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.IsPuppy = data.revert;
await inventory.save();
res.json({
petId: data.petId,
updateCollar: true,
armorSkins: ["", "", ""],
furPatterns: data.revert
? ["", "", ""]
: [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern],
unmature: data.revert
});
};
interface IMaturePetRequest {
petId: string;
revert: boolean;
}

View File

@ -3,10 +3,9 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } 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";
/* /*
**** INPUT **** **** INPUT ****
@ -48,22 +47,16 @@ import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
- [ ] FpsSamples - [ ] FpsSamples
*/ */
//move credit calc in here, return MissionRewards: [] if no reward info //move credit calc in here, return MissionRewards: [] if no reward info
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => { export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(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(accountId); const inventory = await getInventory(accountId);
const firstCompletion = missionReport.SortieId const inventoryUpdates = addMissionInventoryUpdates(inventory, missionReport);
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if ( if (missionReport.MissionStatus !== "GS_SUCCESS") {
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
inventory.RewardSeed = generateRewardSeed();
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true); const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({ res.json({
@ -73,16 +66,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return; return;
} }
const { const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport);
MissionRewards,
inventoryChanges,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
inventory.RewardSeed = generateRewardSeed();
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true); const inventoryResponse = await getInventoryResponse(inventory, true);
@ -93,11 +78,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
MissionRewards, MissionRewards,
...credits, ...credits,
...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
SyndicateXPItemReward, });
AffiliationMods,
ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse);
}; };
/* /*

View File

@ -1,29 +1,30 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
getInventory, import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService";
updateCurrency,
addEquipment, const modularWeaponTypes: Record<string, TEquipmentKey> = {
addMiscItems, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns",
applyDefaultUpgrades, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam": "LongGuns",
occupySlot, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryLauncher": "LongGuns",
productCategoryToInventoryBin, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun": "LongGuns",
combineInventoryChanges, "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimarySniper": "LongGuns",
addSpecialItem "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary": "Pistols",
} from "@/src/services/inventoryService"; "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam": "Pistols",
import { IInventoryChanges } from "@/src/types/purchaseTypes"; "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun": "Pistols",
import { getDefaultUpgrades } from "@/src/services/itemDataService"; "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon": "Melee",
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; "/Lotus/Weapons/Sentients/OperatorAmplifiers/OperatorAmpWeapon": "OperatorAmps",
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit": "Hoverboards",
import { getRandomInt } from "@/src/services/rngService"; "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit": "MoaPets",
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus"; "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": "MoaPets",
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": "MoaPets",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": "MoaPets"
};
interface IModularCraftRequest { interface IModularCraftRequest {
WeaponType: string; WeaponType: string;
Parts: string[]; Parts: string[];
isWebUi?: boolean;
} }
export const modularWeaponCraftingController: RequestHandler = async (req, res) => { export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
@ -35,163 +36,30 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const category = modularWeaponTypes[data.WeaponType]; const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
let defaultUpgrades: IDefaultUpgrade[] | undefined; // Give weapon
const defaultOverwrites: Partial<IEquipmentDatabase> = { const weapon = addEquipment(inventory, category, data.WeaponType, data.Parts);
ModularParts: data.Parts
};
const inventoryChanges: IInventoryChanges = {};
if (category == "KubrowPets") {
const traits = {
"/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit": {
BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareBase",
SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareSecondary",
TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareTertiary",
AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareAccent",
EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorRareEyes",
FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault",
Personality: data.WeaponType,
BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType",
Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadC"
},
"/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit": {
BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonBase",
SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonSecondary",
TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonTertiary",
AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonAccent",
EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorUncommonEyes",
FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault",
Personality: data.WeaponType,
BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType",
Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadB"
},
"/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit": {
BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareBase",
SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareSecondary",
TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareTertiary",
AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareAccent",
EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorRareEyes",
FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault",
Personality: data.WeaponType,
BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType",
Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadA"
},
"/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit": {
BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonBase",
SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonSecondary",
TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonTertiary",
AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonAccent",
EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorUncommonEyes",
FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault",
Personality: data.WeaponType,
BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType",
Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadB"
},
"/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit": {
BaseColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonBase",
SecondaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonSecondary",
TertiaryColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonTertiary",
AccentColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonAccent",
EyeColor: "/Lotus/Types/Game/InfestedPredatorPet/Colors/InfestedPredatorColorCommonEyes",
FurPattern: "/Lotus/Types/Game/InfestedPredatorPet/Patterns/InfestedPredatorPatternDefault",
Personality: data.WeaponType,
BodyType: "/Lotus/Types/Game/KubrowPet/BodyTypes/InfestedKubrowPetBodyType",
Head: "/Lotus/Types/Game/InfestedPredatorPet/Heads/InfestedPredatorHeadC"
},
"/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit": {
BaseColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonBase",
SecondaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonSecondary",
TertiaryColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonTertiary",
AccentColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonAccent",
EyeColor: "/Lotus/Types/Game/InfestedKavatPet/Colors/InfestedKavatColorCommonEyes",
FurPattern: "/Lotus/Types/Game/InfestedKavatPet/Patterns/InfestedCritterPatternDefault",
Personality: data.WeaponType,
BodyType: "/Lotus/Types/Game/CatbrowPet/BodyTypes/InfestedCatbrowPetRegularBodyType",
Head: "/Lotus/Types/Game/InfestedKavatPet/Heads/InfestedCritterHeadA"
}
}[data.WeaponType];
if (!traits) {
throw new Error(`unknown KubrowPets type: ${data.WeaponType}`);
}
defaultOverwrites.Details = {
Name: "",
IsPuppy: false,
HasCollar: true,
PrintsRemaining: 2,
Status: Status.StatusStasis,
HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000),
IsMale: !!getRandomInt(0, 1),
Size: getRandomInt(70, 100) / 100,
DominantTraits: traits,
RecessiveTraits: traits
};
// Only save mutagen & antigen in the ModularParts.
defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]];
const meta = ExportSentinels[data.WeaponType];
for (const specialItem of meta.exalted!) {
addSpecialItem(inventory, specialItem, inventoryChanges);
}
defaultUpgrades = meta.defaultUpgrades;
} else {
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);
addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
// Remove credits & parts // Remove credits & parts
const miscItemChanges = []; const miscItemChanges = [];
let currencyChanges = {};
if (!data.isWebUi) {
for (const part of data.Parts) { for (const part of data.Parts) {
miscItemChanges.push({ miscItemChanges.push({
ItemType: part, ItemType: part,
ItemCount: -1 ItemCount: -1
}); });
} }
currencyChanges = updateCurrency( const currencyChanges = updateCurrency(
inventory, inventory,
category == "Hoverboards" || category == "Hoverboards" || category == "MoaPets" ? 5000 : 4000,
category == "MoaPets" ||
category == "LongGuns" ||
category == "Pistols" ||
category == "KubrowPets"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false false
); );
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
}
await inventory.save(); await inventory.save();
// Tell client what we did // Tell client what we did
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
...inventoryChanges,
...currencyChanges, ...currencyChanges,
[category]: [weapon],
MiscItems: miscItemChanges MiscItems: miscItemChanges
} }
}); });

View File

@ -1,183 +1,8 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportWeapons } from "warframe-public-export-plus"; import modularWeaponSale from "@/static/fixed_responses/modularWeaponSale.json";
import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { CRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import {
addEquipment,
applyDefaultUpgrades,
getInventory,
occupySlot,
productCategoryToInventoryBin,
updateCurrency
} from "@/src/services/inventoryService";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => { const modularWeaponSaleController: RequestHandler = (_req, res) => {
const partTypeToParts: Record<string, string[]> = {}; res.json(modularWeaponSale);
for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if (
data.partType &&
data.premiumPrice &&
!data.excludeFromCodex // exclude pvp variants
) {
partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName);
}
}
if (req.query.op == "SyncAll") {
res.json({
SaleInfos: getSaleInfos(partTypeToParts, Math.trunc(Date.now() / 86400000))
});
} else if (req.query.op == "Purchase") {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IModularWeaponPurchaseRequest>(String(req.body));
const weaponInfo = getSaleInfos(partTypeToParts, payload.Revision).find(x => x.Name == payload.SaleName)!
.Weapons[payload.ItemIndex];
const category = modularWeaponTypes[weaponInfo.ItemType];
const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(inventory, category, weaponInfo.ItemType, {
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
ModularParts: weaponInfo.ModularParts,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, true),
...updateCurrency(inventory, weaponInfo.PremiumPrice, true)
};
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
} else {
throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`);
}
}; };
const getSaleInfos = (partTypeToParts: Record<string, string[]>, day: number): IModularWeaponSaleInfo[] => { export { modularWeaponSaleController };
const kitgunIsPrimary: boolean = (day & 1) != 0;
return [
getModularWeaponSale(
partTypeToParts,
day,
"Ostron",
["LWPT_HILT", "LWPT_BLADE", "LWPT_HILT_WEIGHT"],
() => "/Lotus/Weapons/Ostron/Melee/LotusModularWeapon"
),
getModularWeaponSale(
partTypeToParts,
day,
"SolarisUnitedHoverboard",
["LWPT_HB_DECK", "LWPT_HB_ENGINE", "LWPT_HB_FRONT", "LWPT_HB_JET"],
() => "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit"
),
getModularWeaponSale(
partTypeToParts,
day,
"SolarisUnitedMoaPet",
["LWPT_MOA_LEG", "LWPT_MOA_HEAD", "LWPT_MOA_ENGINE", "LWPT_MOA_PAYLOAD"],
() => "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit"
),
getModularWeaponSale(
partTypeToParts,
day,
"SolarisUnitedKitGun",
[
kitgunIsPrimary ? "LWPT_GUN_PRIMARY_HANDLE" : "LWPT_GUN_SECONDARY_HANDLE",
"LWPT_GUN_BARREL",
"LWPT_GUN_CLIP"
],
(parts: string[]) => {
const barrel = parts[1];
const gunType = ExportWeapons[barrel].gunType!;
if (kitgunIsPrimary) {
return {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam"
}[gunType];
} else {
return {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
}[gunType];
}
}
)
];
};
const priceFactor: Record<string, number> = {
Ostron: 0.9,
SolarisUnitedHoverboard: 0.85,
SolarisUnitedMoaPet: 0.95,
SolarisUnitedKitGun: 0.9
};
const getModularWeaponSale = (
partTypeToParts: Record<string, string[]>,
day: number,
name: string,
partTypes: string[],
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
let partsCost = 0;
for (const part of parts) {
partsCost += ExportWeapons[part].premiumPrice!;
}
return {
Name: name,
Expiry: toMongoDate(new Date((day + 1) * 86400000)),
Revision: day,
Weapons: [
{
ItemType: getItemType(parts),
PremiumPrice: Math.trunc(partsCost * priceFactor[name]),
ModularParts: parts
}
]
};
};
interface IModularWeaponSaleInfo {
Name: string;
Expiry: IMongoDate;
Revision: number;
Weapons: IModularWeaponSaleItem[];
}
interface IModularWeaponSaleItem {
ItemType: string;
PremiumPrice: number;
ModularParts: string[];
}
interface IModularWeaponPurchaseRequest {
SaleName: string;
ItemIndex: number;
Revision: number;
ItemName: string;
PolarizeSlot: number;
PolarizeValue: ArtifactPolarity;
}

View File

@ -12,17 +12,15 @@ export const nameWeaponController: 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 body = getJSONfromString<INameWeaponRequest>(String(req.body)); const body = getJSONfromString<INameWeaponRequest>(String(req.body));
const item = inventory[req.query.Category as string as TEquipmentKey].id(req.query.ItemId as string)!; const item = inventory[req.query.Category as string as TEquipmentKey].find(
item => item._id.toString() == (req.query.ItemId as string)
)!;
if (body.ItemName != "") { if (body.ItemName != "") {
item.ItemName = body.ItemName; item.ItemName = body.ItemName;
} else { } else {
item.ItemName = undefined; item.ItemName = undefined;
} }
const currencyChanges = updateCurrency( const currencyChanges = updateCurrency(inventory, "webui" in req.query ? 0 : 15, true);
inventory,
req.query.Category == "Horses" || "webui" in req.query ? 0 : 15,
true
);
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: currencyChanges InventoryChanges: currencyChanges

View File

@ -1,314 +0,0 @@
import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getKnifeUpgrade,
getNemesisPasscode,
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IInnateDamageFingerprint,
IInventoryClient,
INemesisClient,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
} from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const nemesisController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
if ((req.query.mode as string) == "f") {
const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
const inventory = await getInventory(accountId, body.Category + " WeaponBin");
const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!;
const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!;
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
// Update destination damage type if desired
if (body.UseSourceDmgType) {
destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
}
// Upgrade destination damage value
const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
let newDamage = Math.max(destDamage, sourceDamage) * 1.1;
if (newDamage >= 0.5794998) {
newDamage = 0.6;
}
destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
// Commit fingerprint
destWeapon.UpgradeFingerprint = JSON.stringify(destFingerprint);
// Remove source weapon
inventory[body.Category].pull({ _id: body.SourceWeapon.$oid });
freeUpSlot(inventory, InventorySlot.WEAPONS);
await inventory.save();
res.json({
InventoryChanges: {
[body.Category]: [destWeapon.toJSON()],
RemovedIdItems: [{ ItemId: body.SourceWeapon }]
}
});
} else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[0]) {
guessResult = 1 + i;
break;
}
}
} else {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i]) {
++guessResult;
}
}
}
res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
// Add to GuessHistory
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(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
);
// Increase antivirus if correct antivirus mod is installed
const response: IKnifeResponse = {};
if (result1 == 0 || result2 == 0 || result3 == 0) {
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!;
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
}
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
}
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
await inventory.save();
res.json(response);
} else {
const passcode = getNemesisPasscode(inventory.Nemesis!);
if (passcode[body.position] != body.guess) {
res.end();
} else {
inventory.Nemesis!.Rank += 1;
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
await inventory.save();
res.json({ RankIncrease: 1 });
}
}
} else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body
const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp);
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
const weapons = getWeaponsForManifest(body.target.manifest);
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx;
do {
const weapon = weapons[weaponIdx];
if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
break;
}
weaponIdx = (weaponIdx + 1) % weapons.length;
} while (weaponIdx != initialWeaponIdx);
}
inventory.Nemesis = {
fp: body.target.fp,
manifest: body.target.manifest,
KillingSuit: body.target.KillingSuit,
killingDamageType: body.target.killingDamageType,
ShoulderHelmet: body.target.ShoulderHelmet,
WeaponIdx: weaponIdx,
AgentIdx: body.target.AgentIdx,
BirthNode: body.target.BirthNode,
Faction: body.target.Faction,
Rank: 0,
k: false,
Traded: false,
d: new Date(),
InfNodes: getInfNodes(body.target.Faction, 0),
GuessHistory: [],
Hints: [],
HintProgress: 0,
Weakened: body.target.Weakened,
PrevOwners: 0,
HenchmenKilled: 0,
SecondInCommand: body.target.SecondInCommand,
MissionCount: 0,
LastEnc: 0
};
await inventory.save();
res.json({
target: inventory.toJSON().Nemesis
});
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [
{
Node: showdownNodes[inventory.Nemesis!.Faction],
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const response: IKnifeResponse & { target: INemesisClient } = {
target: inventory.toJSON<IInventoryClient>().Nemesis!
};
// Consume charge of the correct requiem mod(s)
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 modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
for (const modType of modTypes) {
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
await inventory.save();
res.json(response);
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
}
};
interface IValenceFusionRequest {
DestWeapon: IOid;
SourceWeapon: IOid;
Category: TEquipmentKey;
UseSourceDmgType: boolean;
}
interface INemesisStartRequest {
target: {
fp: number | bigint;
manifest: string;
KillingSuit: string;
killingDamageType: number;
ShoulderHelmet: string;
DisallowedWeapons: string[];
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Rank: number;
k: boolean;
Traded: boolean;
d: IMongoDate;
InfNodes: [];
GuessHistory: [];
Hints: [];
HintProgress: number;
Weakened: boolean;
PrevOwners: number;
HenchmenKilled: number;
MissionCount?: number; // Added in 38.5.0
LastEnc?: number; // Added in 38.5.0
SecondInCommand: boolean;
};
}
interface INemesisPrespawnCheckRequest {
guess: number[]; // .length == 3
potency?: number[];
}
interface INemesisRequiemRequest {
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
position: number; // grn/crp: 0-2 | coda: 0
// knife field provided for coda only
knife?: IKnife;
}
// interface INemesisWeakenRequest {
// target: INemesisClient;
// knife: IKnife;
// }
interface IKnife {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
}

View File

@ -1,124 +0,0 @@
import {
getDojoClient,
getGuildForRequestEx,
getVaultMiscItemCount,
hasAccessToDojo,
hasGuildPermission,
processDojoBuildMaterialsGathered,
scaleRequiredCount
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const request = JSON.parse(String(req.body)) as IPlaceDecoInComponentRequest;
const component = guild.DojoComponents.id(request.ComponentId)!;
if (component.DecoCapacity === undefined) {
component.DecoCapacity = Object.values(ExportDojoRecipes.rooms).find(
x => x.resultType == component.pf
)!.decoCapacity;
}
component.Decos ??= [];
if (request.MoveId) {
const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
deco.Pos = request.Pos;
deco.Rot = request.Rot;
deco.Scale = request.Scale;
} else {
const deco =
component.Decos[
component.Decos.push({
_id: new Types.ObjectId(),
Type: request.Type,
Pos: request.Pos,
Rot: request.Rot,
Scale: request.Scale,
Name: request.Name,
Sockets: request.Sockets
}) - 1
];
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type);
if (meta) {
if (meta.capacityCost) {
component.DecoCapacity -= meta.capacityCost;
}
} else {
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0];
if (deco.Sockets !== undefined) {
guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -=
1;
} else {
guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1;
}
}
if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
deco.CompletionTime = new Date();
if (meta) {
processDojoBuildMaterialsGathered(guild, meta);
}
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) {
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let enoughMiscItems = true;
for (const ingredient of meta.ingredients) {
if (
getVaultMiscItemCount(guild, ingredient.ItemType) <
scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
enoughMiscItems = false;
break;
}
}
if (enoughMiscItems) {
guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price);
deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
deco.MiscItems = [];
for (const ingredient of meta.ingredients) {
guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -=
scaleRequiredCount(guild.Tier, ingredient.ItemCount);
deco.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount)
});
}
deco.CompletionTime = new Date(Date.now() + meta.time * 1000);
processDojoBuildMaterialsGathered(guild, meta);
}
}
}
}
}
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface IPlaceDecoInComponentRequest {
ComponentId: string;
Revision: number;
Type: string;
Pos: number[];
Rot: number[];
Scale?: number;
Name?: string;
Sockets?: number;
MoveId?: string;
ShipDeco?: boolean;
VaultDeco?: boolean;
}

View File

@ -1,9 +0,0 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const playedParkourTutorialController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true });
res.end();
};

View File

@ -6,7 +6,7 @@ import { RequestHandler } from "express";
export const playerSkillsController: RequestHandler = async (req, res) => { export const playerSkillsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PlayerSkills"); const inventory = await getInventory(accountId);
const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body)); const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];

View File

@ -1,75 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { GuildAd, GuildMember } from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addVaultMiscItems,
getGuildForRequestEx,
getVaultMiscItemCount,
hasGuildPermissionEx
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
export const postGuildAdvertisementController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId MiscItems");
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!;
if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) {
res.status(400).end();
return;
}
const payload = getJSONfromString<IPostGuildAdvertisementRequest>(String(req.body));
// Handle resource cost
const vendor = getVendorManifestByTypeName(
"/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest"
)!;
const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!;
if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) {
addVaultMiscItems(guild, [
{
ItemType: offer.ItemPrices![0].ItemType,
ItemCount: offer.ItemPrices![0].ItemCount * -1
}
]);
} else {
const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType);
if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) {
res.status(400).json("Insufficient funds");
return;
}
miscItem.ItemCount -= offer.ItemPrices![0].ItemCount;
addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]);
await guildMember.save();
await inventory.save();
}
// Create or update ad
await GuildAd.findOneAndUpdate(
{ GuildId: guild._id },
{
Emblem: guild.Emblem,
Expiry: new Date(Date.now() + 12 * 3600 * 1000),
Features: payload.Features,
GuildName: guild.Name,
MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }),
RecruitMsg: payload.RecruitMsg,
Tier: guild.Tier
},
{ upsert: true }
);
res.end();
};
interface IPostGuildAdvertisementRequest {
Features: number;
RecruitMsg: string;
Languages: string[];
PurchaseParams: IPurchaseParams;
}

View File

@ -50,7 +50,6 @@ const qualityKeywordToNumber: Record<VoidProjectionQuality, number> = {
// e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"] // e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"]
const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => { const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => {
const relic: IRelic | undefined = ExportRelics[typeName]; const relic: IRelic | undefined = ExportRelics[typeName];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!relic) { if (!relic) {
throw new Error(`Unknown projection ${typeName}`); throw new Error(`Unknown projection ${typeName}`);
} }

View File

@ -1,24 +1,19 @@
import { config } from "@/src/services/configService"; import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportDojoRecipes } from "warframe-public-export-plus";
export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => { export const queueDojoComponentDestructionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const guild = await getGuildForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const componentId = req.query.componentId as string; const componentId = req.query.componentId as string;
const component = guild.DojoComponents!.splice(
guild.DojoComponents.id(componentId)!.DestructionTime = new Date( guild.DojoComponents!.findIndex(x => x._id.toString() === componentId),
(Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000 1
); )[0];
const room = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf);
if (room) {
guild.DojoCapacity -= room.capacity;
guild.DojoEnergy -= room.energy;
}
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0, componentId)); res.json(getDojoClient(guild, 1));
}; };

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