Compare commits

..

3 Commits
main ... sortid

Author SHA1 Message Date
b15ca46541 simplify
All checks were successful
Build / build (18) (push) Successful in 1m21s
Build / build (20) (push) Successful in 1m4s
Build / build (22) (push) Successful in 1m22s
Build / build (20) (pull_request) Successful in 45s
Build / build (18) (pull_request) Successful in 1m22s
Build / build (22) (pull_request) Successful in 1m22s
2025-04-09 16:03:04 +02:00
508440e0a6 codify assumption
All checks were successful
Build / build (20) (push) Successful in 45s
Build / build (18) (push) Successful in 1m23s
Build / build (22) (push) Successful in 1m17s
Build / build (18) (pull_request) Successful in 52s
Build / build (20) (pull_request) Successful in 1m23s
Build / build (22) (pull_request) Successful in 1m19s
2025-04-09 15:49:49 +02:00
41623807e1 chore: improve changeDojoRoot
All checks were successful
Build / build (18) (push) Successful in 52s
Build / build (20) (push) Successful in 1m20s
Build / build (18) (pull_request) Successful in 48s
Build / build (22) (push) Successful in 1m22s
Build / build (20) (pull_request) Successful in 1m21s
Build / build (22) (pull_request) Successful in 42s
Using SortId instead of actually changing the component ids.
What's strange is that providing/omitting SortId does seem to make a difference in regards to deco positioning, which is presumably what the POST body would be for. I've opted to simply always provide the SortId in hopes that this avoids the need for repositioning entirely.
2025-04-09 15:44:49 +02:00
415 changed files with 18348 additions and 36374 deletions

View File

@ -3,6 +3,3 @@
Dockerfile*
.*
docker-data/
node_modules/
static/data/
logs/

View File

@ -1,48 +1,36 @@
{
"plugins": ["@typescript-eslint", "prettier", "import"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:import/recommended",
"plugin:import/typescript"
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"plugins": ["@typescript-eslint", "prettier"],
"env": {
"browser": true,
"es6": true,
"node": true
},
"rules": {
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/restrict-template-expressions": "error",
"@typescript-eslint/restrict-plus-operands": "error",
"@typescript-eslint/no-unsafe-member-access": "error",
"@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-explicit-any": "off",
"no-loss-of-precision": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"no-loss-of-precision": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-base-to-string": "off",
"no-case-declarations": "error",
"prettier/prettier": "error",
"no-mixed-spaces-and-tabs": "error",
"@typescript-eslint/require-await": "error",
"import/no-named-as-default-member": "off",
"import/no-cycle": "warn",
"@typescript-eslint/no-deprecated": "warn"
"require-await": "off",
"@typescript-eslint/require-await": "error"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"settings": {
"import/extensions": [ ".ts" ],
"import/resolver": {
"typescript": true,
"node": true
}
}
}

2
.gitattributes vendored
View File

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

View File

@ -1,29 +1,21 @@
name: Build
on:
push:
branches: ["main"]
push: {}
pull_request: {}
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: [18, 20, 22]
steps:
- name: Checkout
uses: actions/checkout@v4.1.2
- name: Setup Node.js environment
uses: actions/setup-node@v4.0.2
with:
node-version: ">=20.18.1"
node-version: ${{ matrix.version }}
- run: npm ci
- run: cp config-vanilla.json config.json
- run: cp config.json.example config.json
- run: npm run verify
- run: npm run lint:ci
- 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
- run: npm run lint

View File

@ -18,10 +18,8 @@ jobs:
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/arm64,linux/amd64
platforms: linux/amd64,linux/arm64
push: true
tags: |
openwf/spaceninjaserver:latest
openwf/spaceninjaserver:latest-arm64
openwf/spaceninjaserver:${{ github.sha }}
openwf/spaceninjaserver:${{ github.sha }}-arm64

View File

@ -2,4 +2,3 @@ src/routes/api.ts
static/webui/libs/
*.html
*.md
config-vanilla.json

3
.vscode/launch.json vendored
View File

@ -8,7 +8,8 @@
"type": "node",
"request": "launch",
"name": "Debug and Watch",
"args": ["${workspaceFolder}/scripts/dev.js"],
"runtimeArgs": ["-r", "tsconfig-paths/register", "-r", "ts-node/register", "--watch-path", "src"],
"args": ["${workspaceFolder}/src/index.ts"],
"console": "integratedTerminal"
}
]

View File

@ -1,3 +0,0 @@
{
"typescript.preferences.preferTypeOnlyAutoImports": true
}

View File

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

View File

@ -1,11 +1,26 @@
FROM node:24-alpine3.21
FROM node:18-alpine3.19
RUN apk add --no-cache bash jq
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF
ENV APP_MY_ADDRESS=localhost
ENV APP_HTTP_PORT=80
ENV APP_HTTPS_PORT=443
ENV APP_AUTO_CREATE_ACCOUNT=true
ENV APP_SKIP_STORY_MODE_CHOICE=true
ENV APP_SKIP_TUTORIAL=true
ENV APP_SKIP_ALL_DIALOGUE=true
ENV APP_UNLOCK_ALL_SCANS=true
ENV APP_UNLOCK_ALL_MISSIONS=true
ENV APP_INFINITE_RESOURCES=true
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true
ENV APP_UNLOCK_ALL_SKINS=true
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true
ENV APP_SPOOF_MASTERY_RANK=-1
RUN apk add --no-cache bash sed wget jq
COPY . /app
WORKDIR /app
RUN npm i --omit=dev --omit=optional
RUN date '+%d %B %Y' > BUILD_DATE
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -6,36 +6,10 @@ More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://di
This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled.
- `skipTutorial` affects only newly created accounts, so you may wish to change it before logging in for the first time.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `ircExecutable` can be provided with a relative path to an EXE which will be ran as a child process of SpaceNinjaServer.
- `ircAddress`, `hubAddress`, and `nrsAddress` can be provided if these secondary servers are on a different machine.
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
- `RadioLegionIntermission14Syndicate` for Nora's Mix: Dreams of the Dead
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
- `RadioLegionIntermission4Syndicate` for Nora's Choice
- `RadioLegionIntermission3Syndicate` for Intermission III
- `RadioLegion3Syndicate` for Glassmaker
- `RadioLegionIntermission2Syndicate` for Intermission II
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six
- `worldState.allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.
- `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,33 +1,26 @@
@echo off
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
if %errorlevel% == 0 (
git stash
git checkout -f origin/main
git stash
git reset --hard origin/main
if exist static\data\0\ (
echo Updating stripped assets...
cd static\data\0\
git pull
cd ..\..\..\
)
echo Updating dependencies...
node scripts/raw-precheck.js > NUL
if %errorlevel% == 0 (
call npm i --omit=dev --omit=optional
call npm run raw
) else (
call npm i --omit=dev
call npm run build
if %errorlevel% == 0 (
call npm run start
)
)
echo SpaceNinjaServer seems to have crashed.
if exist static\data\0\ (
echo Updating stripped assets...
cd static\data\0\
git pull
cd ..\..\..\
)
echo Updating dependencies...
call npm i --omit=dev
call npm run build
if %errorlevel% == 0 (
call npm run start
echo SpaceNinjaServer seems to have crashed.
)
:a
pause > nul
goto a

View File

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

View File

@ -1,74 +0,0 @@
{
"mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
"logger": {
"files": true,
"level": "trace"
},
"myAddress": "localhost",
"bindAddress": "0.0.0.0",
"httpPort": 80,
"httpsPort": 443,
"ircExecutable": null,
"ircAddress": null,
"hubAddress": null,
"nrsAddress": null,
"administratorNames": [],
"autoCreateAccount": true,
"skipTutorial": false,
"unlockAllSkins": false,
"fullyStockedVendors": false,
"skipClanKeyCrafting": false,
"unfaithfulBugFixes": {
"ignore1999LastRegionPlayed": false,
"fixXtraCheeseTimer": false,
"useAnniversaryTagForOldGoals": true
},
"worldState": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"tennoLiveRelay": false,
"baroTennoConRelay": false,
"baroAlwaysAvailable": false,
"baroFullyStocked": false,
"varziaFullyStocked": false,
"wolfHunt": null,
"orphixVenom": false,
"longShadow": false,
"hallowedFlame": false,
"anniversary": null,
"hallowedNightmares": false,
"hallowedNightmaresRewardsOverride": 0,
"naberusNightsOverride": null,
"proxyRebellion": false,
"proxyRebellionRewardsOverride": 0,
"voidCorruption2025Week1": false,
"voidCorruption2025Week2": false,
"voidCorruption2025Week3": false,
"voidCorruption2025Week4": false,
"qtccAlerts": false,
"galleonOfGhouls": 0,
"ghoulEmergenceOverride": null,
"plagueStarOverride": null,
"starDaysOverride": null,
"dogDaysOverride": null,
"dogDaysRewardsOverride": null,
"bellyOfTheBeast": false,
"bellyOfTheBeastProgressOverride": 0,
"eightClaw": false,
"eightClawProgressOverride": 0,
"thermiaFracturesOverride": null,
"thermiaFracturesProgressOverride": 0,
"eidolonOverride": "",
"vallisOverride": "",
"duviriOverride": "",
"nightwaveOverride": "",
"allTheFissures": "",
"varziaOverride": "",
"circuitGameModes": null,
"darvoStockMultiplier": 1
},
"dev": {
"keepVendorsExpired": false
}
}

49
config.json.example Normal file
View File

@ -0,0 +1,49 @@
{
"mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
"logger": {
"files": true,
"level": "trace"
},
"myAddress": "localhost",
"httpPort": 80,
"httpsPort": 443,
"NRS": ["localhost"],
"administratorNames": [],
"autoCreateAccount": true,
"skipTutorial": false,
"skipAllDialogue": false,
"unlockAllScans": false,
"unlockAllMissions": false,
"infiniteCredits": false,
"infinitePlatinum": false,
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
"unlockAllSkins": false,
"unlockAllCapturaScenes": false,
"universalPolarityEverywhere": false,
"unlockDoubleCapacityPotatoesEverywhere": false,
"unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false,
"noDailyStandingLimits": false,
"noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true,
"instantResourceExtractorDrones": false,
"noDojoRoomBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"worldState": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true,
"lockTime": 0
}
}

View File

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

View File

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

3491
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,63 +4,43 @@
"description": "WF Emulator",
"main": "index.ts",
"scripts": {
"start": "node --enable-source-maps build/src/index.js",
"build": "tsgo --inlineSourceMap && ncp static/webui build/static/webui",
"build:tsc": "tsc --incremental --inlineSourceMap && ncp static/webui build/static/webui",
"build:dev": "tsgo --inlineSourceMap",
"build:dev:tsc": "tsc --incremental --inlineSourceMap",
"build-and-start": "npm run build && npm run start",
"dev": "node scripts/dev.cjs",
"dev:bun": "bun scripts/dev.cjs",
"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 ",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"verify": "tsgo --noEmit",
"verify:tsc": "tsc --noEmit",
"raw": "node scripts/raw-precheck.js && node --experimental-transform-types src/index.ts",
"raw:bun": "bun src/index.ts",
"lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.cjs",
"fix": "npm run update-translations && npm run prettier"
"update-translations": "cd scripts && node update-translations.js"
},
"license": "GNU",
"type": "module",
"dependencies": {
"body-parser": "^2.2.0",
"chokidar": "^4.0.3",
"@types/express": "^5",
"@types/morgan": "^1.9.9",
"crc-32": "^1.2.2",
"express": "^5",
"json-with-bigint": "^3.4.4",
"json-with-bigint": "^3.2.2",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.93",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.52",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",
"ws": "^8.18.2"
},
"optionalDependencies": {
"@types/body-parser": "^1.19.6",
"@types/express": "^5",
"@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "^7.0.0-dev.20250625.1",
"typescript": "^5.7"
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@rxliuli/tsgo": "^2025.3.31",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3",
"tree-kill": "^1.2.2"
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0"
},
"engines": {
"node": ">=20.18.1"
"node": ">=18.15.0",
"npm": ">=9.5.0"
}
}

View File

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

View File

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

View File

@ -1,11 +1,10 @@
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
// Converted via ChatGPT-4o
/* eslint-disable */
const fs = require("fs");
function extractStrings(content) {
const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
let matches;
const strings = {};
while ((matches = regex.exec(content)) !== null) {
@ -16,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
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 => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
@ -31,13 +30,13 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
const strings = extractStrings(line);
if (Object.keys(strings).length > 0) {
Object.entries(strings).forEach(([key, value]) => {
if (targetStrings.hasOwnProperty(key) && !targetStrings[key].startsWith("[UNTRANSLATED]")) {
if (targetStrings.hasOwnProperty(key)) {
fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`);
} else {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
}
});
} else {
} else if (line.length) {
fs.writeSync(fileHandle, line + "\n");
}
});

View File

@ -1,39 +1,32 @@
import express from "express";
import bodyParser from "body-parser";
import { unknownEndpointHandler } from "./middleware/middleware.ts";
import { requestLogger } from "./middleware/morgenMiddleware.ts";
import { errorHandler } from "./middleware/errorHandler.ts";
import { unknownEndpointHandler } from "@/src/middleware/middleware";
import { requestLogger } from "@/src/middleware/morgenMiddleware";
import { errorHandler } from "@/src/middleware/errorHandler";
import { apiRouter } from "./routes/api.ts";
import { cacheRouter } from "./routes/cache.ts";
import { customRouter } from "./routes/custom.ts";
import { dynamicController } from "./routes/dynamic.ts";
import { payRouter } from "./routes/pay.ts";
import { statsRouter } from "./routes/stats.ts";
import { webuiRouter } from "./routes/webui.ts";
import { apiRouter } from "@/src/routes/api";
import { cacheRouter } from "@/src/routes/cache";
import { customRouter } from "@/src/routes/custom";
import { dynamicController } from "@/src/routes/dynamic";
import { payRouter } from "@/src/routes/pay";
import { statsRouter } from "@/src/routes/stats";
import { webuiRouter } from "@/src/routes/webui";
const app = express();
app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
// The client patch is expected to decrypt it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
// 38.5.0 introduced "ezip" for encrypted body blobs.
// 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"] = undefined;
}
// U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
// U17 sets no Content-Type at all, which Express also doesn't like.
if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
req.headers["content-type"] = "application/octet-stream";
}
next();
});
app.use(bodyParser.raw());
app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text({ limit: "4mb" }));
app.use(bodyParser.text());
app.use(requestLogger);
app.use("/api", apiRouter);

View File

@ -1,21 +1,16 @@
export const EPOCH = 1734307200_000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be the start of winter in 1999 iteration 0
const millisecondsPerSecond = 1000;
const secondsPerMinute = 60;
const minutesPerHour = 60;
const hoursPerDay = 24;
const daysPerWeek = 7;
const unixSecond = millisecondsPerSecond;
const unixMinute = secondsPerMinute * millisecondsPerSecond;
const unixHour = unixMinute * minutesPerHour;
const unixDay = hoursPerDay * unixHour;
const unixWeek = daysPerWeek * unixDay;
export const unixTimesInMs = {
second: unixSecond,
minute: unixMinute,
hour: unixHour,
day: unixDay,
week: unixWeek
day: unixDay
};

View File

@ -1,6 +1,6 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
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);

View File

@ -5,11 +5,11 @@ import {
hasGuildPermission,
removeDojoDeco,
removeDojoRoom
} from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
} 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);
@ -31,13 +31,12 @@ export const abortDojoComponentController: RequestHandler = async (req, res) =>
if (request.DecoId) {
removeDojoDeco(guild, request.ComponentId, request.DecoId);
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
} else {
await removeDojoRoom(guild, request.ComponentId);
await guild.save();
res.json(await getDojoClient(guild, 0));
}
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
};
interface IAbortDojoComponentRequest {

View File

@ -1,13 +1,8 @@
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission
} from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
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);

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import type { RequestHandler } from "express";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const addFriendImageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);

View File

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

View File

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

View File

@ -1,16 +1,13 @@
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
import { Guild, GuildMember } from "../../models/guildModel.ts";
import { Account } from "../../models/loginModel.ts";
import { addInventoryDataToFriendInfo, areFriends } from "../../services/friendService.ts";
import { hasGuildPermission } from "../../services/guildService.ts";
import { createMessage } from "../../services/inboxService.ts";
import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts";
import type { IOid } from "../../types/commonTypes.ts";
import type { IGuildMemberClient } from "../../types/guildTypes.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { logger } from "../../utils/logger.ts";
import type { RequestHandler } from "express";
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) => {
@ -25,18 +22,15 @@ export const addToGuildController: RequestHandler = async (req, res) => {
return;
}
const senderAccount = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "Settings");
if (
inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" ||
(inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" &&
!(await areFriends(account._id, senderAccount._id)))
) {
// 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");
}
@ -65,7 +59,7 @@ export const addToGuildController: RequestHandler = async (req, res) => {
}
],
sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: payload.GuildId.$oid,
highPriority: true,
acceptAction: "GUILD_INVITE",
@ -77,11 +71,10 @@ export const addToGuildController: RequestHandler = async (req, res) => {
const member: IGuildMemberClient = {
_id: { $oid: account._id.toString() },
DisplayName: account.DisplayName,
LastLogin: toMongoDate(account.LastLogin),
Rank: 7,
Status: 2
};
await addInventoryDataToFriendInfo(member);
await fillInInventoryDataForGuildMember(member);
res.json({ NewMember: member });
} else if ("RequestMsg" in payload) {
// Player applying to join a clan

View File

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

View File

@ -1,22 +0,0 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { RequestHandler } from "express";
export const apartmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId, "Apartment");
const response: IApartmentResponse = {};
if (req.query.backdrop !== undefined) {
response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
}
if (req.query.soundscape !== undefined) {
response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
}
await personalRooms.save();
res.json(response);
};
interface IApartmentResponse {
NewBackdropItem?: string;
NewSoundscapeItem?: string;
}

View File

@ -1,8 +1,8 @@
import type { RequestHandler } from "express";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory, addMods } from "../../services/inventoryService.ts";
import type { IOid } from "../../types/commonTypes.ts";
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMods } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes";
export const arcaneCommonController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);

View File

@ -1,8 +1,8 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import { colorToShard, combineColors, shardToColor } from "../../helpers/shardHelper.ts";
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { colorToShard, combineColors, shardToColor } from "@/src/helpers/shardHelper";
export const archonFusionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);

View File

@ -1,12 +1,11 @@
import { fromOid, toOid } from "../../helpers/inventoryHelpers.ts";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "../../helpers/rivenHelper.ts";
import { addMiscItems, addMods, getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "../../services/rngService.ts";
import type { IUpgradeFromClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
import type { TRarity } from "warframe-public-export-plus";
import { ExportBoosterPacks, ExportUpgrades } from "warframe-public-export-plus";
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);
@ -25,11 +24,11 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
]);
payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
});
const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
@ -58,16 +57,12 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
} else {
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
}
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") {
@ -77,33 +72,22 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
}
});
let newModType: string | undefined;
for (const specialModSet of specialModSets) {
if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
newModType = getRandomElement(specialModSet);
break;
// 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 });
}
}
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;
}
});
const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
addMods(inventory, [
{
ItemType: newModType,
@ -129,41 +113,20 @@ const getRandomRawRivenType = (): string => {
};
interface IArtifactTransmutationRequest {
Upgrade: IUpgradeFromClient;
Upgrade: IAgnosticUpgradeClient;
LevelDiff: number;
Consumed: IUpgradeFromClient[];
Consumed: IAgnosticUpgradeClient[];
Cost: number;
FusionPointCost: number;
RivenTransmute?: boolean;
}
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"
]
];
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}

View File

@ -1,9 +1,9 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import { addMods, getInventory } from "../../services/inventoryService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { IInventoryClient, IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { addMods, getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
export const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -24,6 +24,7 @@ export const artifactsController: RequestHandler = async (req, res) => {
if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
inventory.markModified(`Upgrades.${itemIndex}.UpgradeFingerprint`);
} else {
itemIndex =
Upgrades.push({
@ -34,10 +35,10 @@ export const artifactsController: RequestHandler = async (req, res) => {
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
if (!inventory.infiniteCredits) {
if (!config.infiniteCredits) {
inventory.RegularCredits -= Cost;
}
if (!inventory.infiniteEndo) {
if (!config.infiniteEndo) {
inventory.FusionPoints -= FusionPointCost;
}
@ -58,7 +59,6 @@ export const artifactsController: RequestHandler = async (req, res) => {
}
res.send(itemId);
broadcastInventoryUpdate(req);
};
interface IArtifactsRequest {

View File

@ -1,9 +1,9 @@
import { GuildAd } from "../../models/guildModel.ts";
import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
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);

View File

@ -1,16 +1,10 @@
import type { RequestHandler } from "express";
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission
} from "../../services/guildService.ts";
import { logger } from "../../utils/logger.ts";
import type { IDojoComponentDatabase } from "../../types/guildTypes.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { RequestHandler } from "express";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { logger } from "@/src/utils/logger";
import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes";
import { Types } from "mongoose";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const changeDojoRootController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);

View File

@ -1,8 +1,8 @@
import { GuildMember } from "../../models/guildModel.ts";
import { getGuildForRequest, hasGuildPermissionEx } from "../../services/guildService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
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);

View File

@ -1,12 +1,16 @@
import { getAccountForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { RequestHandler } from "express";
export const checkDailyMissionBonusController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const today = Math.trunc(Date.now() / 86400000) * 86400;
if (account.DailyFirstWinDate != today) {
res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n");
} else {
res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n");
}
const checkDailyMissionBonusController: RequestHandler = (_req, res) => {
const data = Buffer.from([
0x44, 0x61, 0x69, 0x6c, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x3a,
0x31, 0x2d, 0x44, 0x61, 0x69, 0x6c, 0x79, 0x50, 0x56, 0x50, 0x57, 0x69, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73,
0x3a, 0x31, 0x0a
]);
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": data.length
});
res.end(data);
};
export { checkDailyMissionBonusController };

View File

@ -1,307 +1,139 @@
//this is a controller for the claimCompletedRecipe route
//it will claim a recipe for the user
import type { RequestHandler } from "express";
import { logger } from "../../utils/logger.ts";
import { getRecipe } from "../../services/itemDataService.ts";
import type { IOidWithLegacySupport } from "../../types/commonTypes.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { TAccountDocument } from "../../services/loginService.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { RequestHandler } from "express";
import { logger } from "@/src/utils/logger";
import { getRecipe } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
getInventory,
updateCurrency,
addItem,
addRecipes,
occupySlot,
combineInventoryChanges,
addKubrowPetPrint,
addPowerSuit,
addEquipment
} from "../../services/inventoryService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import type { IRecipe } from "warframe-public-export-plus";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
combineInventoryChanges
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOidWithLegacySupport[];
}
interface IClaimCompletedRecipeResponse {
InventoryChanges: IInventoryChanges;
BrandedSuits?: IOidWithLegacySupport[];
RecipeIds: IOid[];
}
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString());
const resp: IClaimCompletedRecipeResponse = {
InventoryChanges: {}
};
for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
}
const accountId = await getAccountIdForRequest(req);
if (!accountId) throw new Error("no account id");
//check recipe is indeed ready to be completed
// if (pendingRecipe.CompletionDate > new Date()) {
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// }
inventory.PendingRecipes.pull(pendingRecipe._id);
const recipe = getRecipe(pendingRecipe.ItemType);
if (!recipe) {
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
}
if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {};
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
return;
}
await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
const inventory = await getInventory(accountId);
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
}
await inventory.save();
res.json(resp);
};
const claimCompletedRecipe = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase,
resp: IClaimCompletedRecipeResponse,
rush: any
): Promise<void> => {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
//check recipe is indeed ready to be completed
// if (pendingRecipe.CompletionDate > new Date()) {
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
// }
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
inventory.PendingRecipes.pull(pendingRecipe._id);
const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
if (pendingLoadoutIndex != -1) {
const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
if (loadoutIndex != -1) {
inventory.SpectreLoadouts.splice(loadoutIndex, 1);
const recipe = getRecipe(pendingRecipe.ItemType);
if (!recipe) {
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
}
if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
logger.debug(
"moving spectre loadout from pending to active",
inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(
x => x.ItemType == recipe.resultType
);
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
}
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
resp.BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
}
if (recipe.consumeOnUse) {
addRecipes(inventory, [
{
ItemType: pendingRecipe.ItemType,
ItemCount: -1
if (pendingLoadoutIndex != -1) {
const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
if (loadoutIndex != -1) {
inventory.SpectreLoadouts.splice(loadoutIndex, 1);
}
logger.debug(
"moving spectre loadout from pending to active",
inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
);
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
}
]);
}
if (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 =
progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
}
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
if (pet.Details!.HatchDate!.getTime() > Date.now()) {
pet.Details!.HatchDate = new Date();
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
}
let canSetActive = true;
for (const pet of inventory.KubrowPets) {
if (pet.Details!.Status == Status.StatusAvailable) {
canSetActive = false;
break;
}
}
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
addKubrowPetPrint(inventory, pet, resp.InventoryChanges);
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
// Quite the special case here...
// We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
// Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
const umbraModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModC = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
resp.InventoryChanges.Upgrades ??= [];
resp.InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
await addPowerSuit(
inventory,
"/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
let InventoryChanges = {};
if (recipe.consumeOnUse) {
addRecipes(inventory, [
{
Configs: [
{
Upgrades: [
"",
"",
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
]
}
],
XP: 900_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
resp.InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
XP: 900_000
});
addEquipment(
inventory,
"Melee",
"/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
{
Configs: [
{ Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
],
XP: 450_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
resp.InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
XP: 450_000
});
} else {
combineInventoryChanges(
resp.InventoryChanges,
await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
)
);
}
}
if (
inventory.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) {
await refundRecipeIngredients(inventory, resp.InventoryChanges, recipe, pendingRecipe);
}
};
const refundRecipeIngredients = async (
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase
): Promise<void> => {
updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
ItemType: pendingRecipe.ItemType,
ItemCount: -1
}
]);
}
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,
...updateCurrency(inventory, cost, true)
};
}
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
}
await inventory.save();
res.json({ InventoryChanges });
}
};

View File

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

View File

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

View File

@ -1,14 +1,12 @@
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const clearDialogueHistoryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
inventory.DialogueHistory.Resets ??= 0;
inventory.DialogueHistory.Resets += 1;
for (const dialogueName of request.Dialogues) {
const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
if (index != -1) {

View File

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

View File

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

View File

@ -1,10 +1,11 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { IVeiledRivenFingerprint } from "../../helpers/rivenHelper.ts";
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);
@ -26,11 +27,10 @@ export const completeRandomModChallengeController: RequestHandler = async (req,
inventoryChanges.MiscItems = miscItemChanges;
}
// Complete the riven challenge
// Update riven fingerprint to a randomised unveiled state
const upgrade = inventory.Upgrades.id(request.ItemId)!;
const fp = JSON.parse(upgrade.UpgradeFingerprint!) as IVeiledRivenFingerprint;
fp.challenge.Progress = fp.challenge.Required;
upgrade.UpgradeFingerprint = JSON.stringify(fp);
const meta = ExportUpgrades[upgrade.ItemType];
upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta));
await inventory.save();

View File

@ -1,7 +1,7 @@
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
import { getAllianceClient } from "../../services/guildService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
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

View File

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

View File

@ -1,10 +1,11 @@
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { Guild } from "../../models/guildModel.ts";
import { checkClanAscensionHasRequiredContributors } from "../../services/guildService.ts";
import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { config } from "@/src/services/configService";
import { createMessage } from "@/src/services/inboxService";
import { 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) => {
@ -30,13 +31,49 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
guild.CeremonyContributors.push(new Types.ObjectId(accountId));
await checkClanAscensionHasRequiredContributors(guild);
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
if (guild.CeremonyContributors.length == payload.RequiredContributors) {
guild.Class = guild.CeremonyClass!;
guild.CeremonyClass = undefined;
guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
if (!config.fastClanAscension) {
// Send message to all active guild members
const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
for (const member of members) {
// somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
await createMessage(member.accountId, [
{
sndr: guild.Name,
msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
arg: [
{
Key: "RESETDATE",
Tag:
guild.CeremonyResetDate.getUTCMonth() +
"/" +
guild.CeremonyResetDate.getUTCDate() +
"/" +
(guild.CeremonyResetDate.getUTCFullYear() % 100) +
" " +
guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
":" +
guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
}
],
sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
highPriority: true
}
]);
}
}
}
await guild.save();
// Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints");
addFusionPoints(inventory, guild.CeremonyEndo!);
inventory.FusionPoints += guild.CeremonyEndo!;
await inventory.save();
res.json({

View File

@ -1,6 +1,5 @@
import type { TGuildDatabaseDocument } from "../../models/guildModel.ts";
import { GuildMember } from "../../models/guildModel.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
addGuildMemberMiscItemContribution,
getDojoClient,
@ -9,15 +8,14 @@ import {
processDojoBuildMaterialsGathered,
scaleRequiredCount,
setDojoRoomLogFunded
} from "../../services/guildService.ts";
import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IDojoContributable, IGuildMemberDatabase } from "../../types/guildTypes.ts";
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { RequestHandler } from "express";
import type { IDojoBuild } from "warframe-public-export-plus";
import { ExportDojoRecipes } from "warframe-public-export-plus";
} 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;

View File

@ -1,5 +1,10 @@
import type { TGuildDatabaseDocument, TGuildMemberDatabaseDocument } from "../../models/guildModel.ts";
import { Alliance, Guild, GuildMember } from "../../models/guildModel.ts";
import {
Alliance,
Guild,
GuildMember,
TGuildDatabaseDocument,
TGuildMemberDatabaseDocument
} from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addGuildMemberShipDecoContribution,
@ -7,18 +12,17 @@ import {
addVaultMiscItems,
addVaultShipDecos,
getGuildForRequestEx
} from "../../services/guildService.ts";
} from "@/src/services/guildService";
import {
addFusionTreasures,
addMiscItems,
addShipDecorations,
getInventory,
updateCurrency
} from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { ITypeCount } from "../../types/commonTypes.ts";
import type { IFusionTreasure, IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
} 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);

View File

@ -1,10 +1,10 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
import { getAllianceClient } from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
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);

View File

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

View File

@ -1,17 +1,12 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const creditsController: RequestHandler = async (req, res) => {
const inventory = (
await Promise.all([
getAccountIdForRequest(req),
getInventory(
req.query.accountId as string,
"RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits infiniteCredits infinitePlatinum"
)
])
)[1];
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits");
const response = {
RegularCredits: inventory.RegularCredits,
@ -20,10 +15,10 @@ export const creditsController: RequestHandler = async (req, res) => {
PremiumCredits: inventory.PremiumCredits
};
if (inventory.infiniteCredits) {
if (config.infiniteCredits) {
response.RegularCredits = 999999999;
}
if (inventory.infinitePlatinum) {
if (config.infinitePlatinum) {
response.PremiumCreditsFree = 0;
response.PremiumCredits = 999999999;
}

View File

@ -1,54 +0,0 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { 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 NemesisHistory");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
if (data.crewMember.SecondInCommand) {
clearOnCall(inventory);
}
if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
} else {
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;
}
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
if (inventory.NemesisHistory) {
for (const cm of inventory.NemesisHistory) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
}
};

View File

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

View File

@ -1,87 +0,0 @@
import {
addCrewShipSalvagedWeaponSkin,
addCrewShipRawSalvage,
getInventory,
addEquipment
} from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
import type {
ICrewShipComponentFingerprint,
IInnateDamageFingerprint
} from "../../types/inventoryTypes/inventoryTypes.ts";
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import { getRandomInt } from "../../services/rngService.ts";
import type { IFingerprintStat } from "../../helpers/rivenHelper.ts";
import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts";
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,15 +1,12 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { Guild } from "../../models/guildModel.ts";
import { hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountForRequest, getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { logger } from "../../utils/logger.ts";
import type { RequestHandler } from "express";
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 Ranks"))!;
const guild = (await Guild.findById(data.g, "DojoComponents"))!;
const component = guild.DojoComponents.id(data.c)!;
if (req.query.act == "f") {
res.json({
@ -37,19 +34,6 @@ export const customObstacleCourseLeaderboardController: RequestHandler = async (
entry.r = ++r;
}
await guild.save();
res.status(200).end();
} else if (req.query.act == "c") {
// TOVERIFY: What clan permission is actually needed for this?
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.status(400).end();
return;
}
component.Leaderboard = undefined;
await guild.save();
res.status(200).end();
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);

View File

@ -1,8 +1,7 @@
import { getGuildForRequest, hasGuildPermission } from "../../services/guildService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IGuildRank } from "../../types/guildTypes.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
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);

View File

@ -1,6 +1,6 @@
import { AllianceMember, GuildMember } from "../../models/guildModel.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
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

View File

@ -1,6 +1,6 @@
import { GuildMember } from "../../models/guildModel.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
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);

View File

@ -1,17 +1,9 @@
import type { RequestHandler } from "express";
import { deleteSession } from "../../managers/sessionManager.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { version_compare } from "../../helpers/inventoryHelpers.ts";
import { RequestHandler } from "express";
import { deleteSession } from "@/src/managers/sessionManager";
const deleteSessionController: RequestHandler = async (_req, res) => {
const account = await getAccountForRequest(_req);
const deleteSessionController: RequestHandler = (_req, res) => {
deleteSession(_req.query.sessionId as string);
if (account.BuildLabel && version_compare(account.BuildLabel, "2016.07.08.16.56") < 0) {
// Pre-Specters of the Rail
res.send(_req.query.sessionId as string); // Unsure if this is correct, but the client is chill with it
} else {
res.sendStatus(200);
}
res.sendStatus(200);
};
export { deleteSessionController };

View File

@ -3,14 +3,12 @@ import {
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission,
refundDojoDeco,
removeDojoDeco
} from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { logger } from "../../utils/logger.ts";
import type { RequestHandler } from "express";
} 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);
@ -20,20 +18,9 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
res.json({ DojoRequestStatus: -1 });
return;
}
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
if ("DecoType" in request) {
removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else if (request.Act == "cObst") {
const component = guild.DojoComponents.id(request.ComponentId)!;
if (component.Decos) {
for (const deco of component.Decos) {
refundDojoDeco(guild, component, deco);
}
component.Decos.splice(0, component.Decos.length);
}
} else {
logger.error(`unhandled destroyDojoDeco request`, request);
}
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));
@ -44,8 +31,3 @@ interface IDestroyDojoDecoRequest {
ComponentId: string;
DecoId: string;
}
interface IClearObstacleCourseRequest {
ComponentId: string;
Act: "cObst" | "maybesomethingelsewedontknowabout";
}

View File

@ -1,9 +1,9 @@
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
import { getAccountForRequest } from "../../services/loginService.ts";
import { GuildPermission } from "../../types/guildTypes.ts";
import { parallelForeach } from "../../utils/async-utils.ts";
import { logger } from "../../utils/logger.ts";
import type { RequestHandler } from "express";
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.

View File

@ -1,17 +1,10 @@
import type { TGuildDatabaseDocument } from "../../models/guildModel.ts";
import { GuildMember } from "../../models/guildModel.ts";
import {
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
scaleRequiredCount
} from "../../services/guildService.ts";
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IDojoContributable } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
import type { IDojoBuild } from "warframe-public-export-plus";
import { ExportDojoRecipes } from "warframe-public-export-plus";
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;

View File

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

View File

@ -1,12 +1,13 @@
import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts";
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
import { fromStoreItem } from "../../services/itemDataService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getRandomInt, getRandomWeightedRewardUc } from "../../services/rngService.ts";
import type { IMongoDate, IOid } from "../../types/commonTypes.ts";
import type { IDroneClient } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { RequestHandler } from "express";
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 { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus";
export const dronesController: RequestHandler = async (req, res) => {
@ -38,13 +39,10 @@ export const dronesController: RequestHandler = async (req, res) => {
ActiveDrones: activeDrones
});
} else if ("droneId" in req.query && "systemIndex" in req.query) {
const inventory = await getInventory(
accountId,
"Drones instantResourceExtractorDrones noResourceExtractorDronesDamage"
);
const inventory = await getInventory(accountId, "Drones");
const drone = inventory.Drones.id(req.query.droneId as string)!;
const droneMeta = ExportDrones[drone.ItemType];
drone.DeployTime = inventory.instantResourceExtractorDrones ? new Date(0) : new Date();
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;
@ -53,11 +51,11 @@ export const dronesController: RequestHandler = async (req, res) => {
}
drone.System = parseInt(req.query.systemIndex as string);
const system = ExportSystems[drone.System - 1];
drone.DamageTime = inventory.instantResourceExtractorDrones
drone.DamageTime = config.instantResourceExtractorDrones
? new Date()
: new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage =
!inventory.noResourceExtractorDronesDamage && Math.random() < system.damageChance
Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;
@ -74,7 +72,7 @@ export const dronesController: RequestHandler = async (req, res) => {
);
}
} else {
drone.ResourceCount = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
drone.ResourceCount = 1;
}
await inventory.save();
res.json({});

View File

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

View File

@ -1,8 +1,8 @@
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getInventory, updateEntratiVault } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
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);
@ -11,7 +11,24 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
"EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission"
);
const body = getJSONfromString<IEntratiLabConquestModeRequest>(String(req.body));
updateEntratiVault(inventory);
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.EntratiLabConquestActiveFrameVariants = [];
}
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestUnlocked = 0;
inventory.EchoesHexConquestActiveFrameVariants = [];
inventory.EchoesHexConquestActiveStickers = [];
}
}
if (body.BuyMode) {
inventory.EntratiVaultCountLastPeriod! += 2;
if (body.IsEchoesDeepArchemedea) {
@ -32,7 +49,7 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
}
await inventory.save();
res.json({
EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate!),
EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate),
EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod,
EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked,
EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission,

View File

@ -1,10 +1,9 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { WeaponTypeInternal } from "../../services/itemDataService.ts";
import { getRecipe } from "../../services/itemDataService.ts";
import { EquipmentFeatures } from "../../types/equipmentTypes.ts";
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const evolveWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);

View File

@ -1,62 +0,0 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { logger } from "../../utils/logger.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
export const feedPrinceController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "MiscItems NokkoColony NodeIntrosCompleted");
const payload = getJSONfromString<IFeedPrinceRequest>(String(req.body));
switch (payload.Mode) {
case "r": {
inventory.NokkoColony ??= {
FeedLevel: 0,
JournalEntries: []
};
const InventoryChanges: IInventoryChanges = {};
inventory.NokkoColony.FeedLevel += payload.Amount;
if (
(!inventory.NodeIntrosCompleted.includes("CompletedVision1") && inventory.NokkoColony.FeedLevel > 20) ||
(!inventory.NodeIntrosCompleted.includes("CompletedVision2") && inventory.NokkoColony.FeedLevel > 60)
) {
res.json({
FeedSucceeded: false,
FeedLevel: inventory.NokkoColony.FeedLevel - payload.Amount,
InventoryChanges
} satisfies IFeedPrinceResponse);
} else {
addMiscItem(
inventory,
"/Lotus/Types/Items/MiscItems/MushroomFood",
payload.Amount * -1,
InventoryChanges
);
await inventory.save();
res.json({
FeedSucceeded: true,
FeedLevel: inventory.NokkoColony.FeedLevel,
InventoryChanges
} satisfies IFeedPrinceResponse);
}
break;
}
default:
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown feedPrince mode: ${payload.Mode}`);
}
};
interface IFeedPrinceRequest {
Mode: string; // r
Amount: number;
}
interface IFeedPrinceResponse {
FeedSucceeded: boolean;
FeedLevel: number;
InventoryChanges: IInventoryChanges;
}

View File

@ -1,7 +1,7 @@
import type { RequestHandler } from "express";
import { getSession } from "../../managers/sessionManager.ts";
import { logger } from "../../utils/logger.ts";
import type { IFindSessionRequest } from "../../types/session.ts";
import { RequestHandler } from "express";
import { getSession } from "@/src/managers/sessionManager";
import { logger } from "@/src/utils/logger";
import { IFindSessionRequest } from "@/src/types/session";
export const findSessionsController: RequestHandler = (_req, res) => {
const req = JSON.parse(String(_req.body)) as IFindSessionRequest;

View File

@ -1,9 +1,10 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { addMiscItems, addStanding, getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -30,7 +31,25 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
}
addMiscItems(inventory, miscItemChanges);
if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding);
if (gainedStanding && syndicateTag) {
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();
res.json({
InventoryChanges: {

View File

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

View File

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

View File

@ -1,15 +1,23 @@
import type { RequestHandler } from "express";
import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { addFusionTreasures, addMiscItems, getInventory } from "../../services/inventoryService.ts";
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
interface IFusionTreasureRequest {
oldTreasureName: string;
newTreasureName: string;
}
const parseFusionTreasure = (name: string, count: number): IFusionTreasure => {
const arr = name.split("_");
return {
ItemType: arr[0],
Sockets: parseInt(arr[1], 16),
ItemCount: count
};
};
export const fusionTreasuresController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);

View File

@ -1,83 +0,0 @@
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
import { toStoreItem } from "../../services/itemDataService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { IMongoDate } from "../../types/commonTypes.ts";
import type { IMissionReward } from "../../types/missionTypes.ts";
import type { IGardeningClient, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { 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

@ -1,8 +1,8 @@
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { updateGeneric } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import type { IGenericUpdate } from "../../types/genericUpdate.ts";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { updateGeneric } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IGenericUpdate } from "@/src/types/genericUpdate";
// This endpoint used to be /api/genericUpdate.php, but sometime around the Jade Shadows update, it was changed to /api/updateNodeIntros.php.
// SpaceNinjaServer supports both endpoints right now.

View File

@ -1,8 +1,8 @@
import { Alliance, Guild } from "../../models/guildModel.ts";
import { getAllianceClient } from "../../services/guildService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { RequestHandler } from "express";
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";
export const getAllianceController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -18,7 +18,6 @@ export const getAllianceController: RequestHandler = async (req, res) => {
res.end();
};
// POST request since U27
/*interface IGetAllianceRequest {
memberCount: number;
clanLeaderName: string;

View File

@ -1,10 +1,8 @@
import { DailyDeal } from "../../models/worldStateModel.ts";
import type { RequestHandler } from "express";
import { RequestHandler } from "express";
export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => {
const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!;
export const getDailyDealStockLevelsController: RequestHandler = (req, res) => {
res.json({
StoreItem: req.query.productName,
AmountSold: dailyDeal.AmountSold
AmountSold: 0
});
};

View File

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

View File

@ -1,8 +1,8 @@
import { GuildMember } from "../../models/guildModel.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IGuildMemberClient } from "../../types/guildTypes.ts";
import type { RequestHandler } from "express";
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);

View File

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

View File

@ -1,8 +1,7 @@
import type { RequestHandler } from "express";
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { Guild } from "../../models/guildModel.ts";
import { getDojoClient } from "../../services/guildService.ts";
import { Account } from "../../models/loginModel.ts";
import { Guild } from "@/src/models/guildModel";
import { getDojoClient } from "@/src/services/guildService";
export const getGuildDojoController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId as string;
@ -19,15 +18,14 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
_id: new Types.ObjectId(),
pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
ppf: "",
CompletionTime: new Date(Date.now() - 1000),
CompletionTime: new Date(Date.now()),
DecoCapacity: 600
});
await guild.save();
}
const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {};
const account = await Account.findById(req.query.accountId as string);
res.json(await getDojoClient(guild, 0, payload.ComponentId, account?.BuildLabel));
res.json(await getDojoClient(guild, 0, payload.ComponentId));
};
interface IGetGuildDojoRequest {

View File

@ -1,26 +0,0 @@
import type { RequestHandler } from "express";
import { getAccountForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { Guild } from "../../models/guildModel.ts";
export const getGuildEventScoreController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId");
const guild = await Guild.findById(inventory.GuildId);
const goalId = req.query.goalId as string;
if (guild && guild.GoalProgress && goalId) {
const goal = guild.GoalProgress.find(x => x.goalId.toString() == goalId);
if (goal) {
res.json({
Tier: guild.Tier,
GoalProgress: {
Count: goal.Count,
Tag: goal.Tag,
_id: { $oid: goal.goalId }
}
});
return;
}
}
res.json({});
};

View File

@ -1,9 +1,9 @@
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
import { Guild } from "../../models/guildModel.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IMongoDate } from "../../types/commonTypes.ts";
import type { RequestHandler } from "express";
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";
export const getGuildLogController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);

View File

@ -1,20 +1,16 @@
import { toOid } from "../../helpers/inventoryHelpers.ts";
import { Account, Ignore } from "../../models/loginModel.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IFriendInfo } from "../../types/friendTypes.ts";
import { parallelForeach } from "../../utils/async-utils.ts";
import type { RequestHandler } from "express";
import { RequestHandler } from "express";
export const getIgnoredUsersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const ignores = await Ignore.find({ ignorer: accountId });
const ignoredUsers: IFriendInfo[] = [];
await parallelForeach(ignores, async ignore => {
const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
ignoredUsers.push({
_id: toOid(ignore.ignoree),
DisplayName: ignoreeAccount.DisplayName + ""
});
const getIgnoredUsersController: RequestHandler = (_req, res) => {
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": "3"
});
res.json({ IgnoredUsers: ignoredUsers });
res.end(
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,12 +1,14 @@
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { generateRewardSeed } from "../../services/rngService.ts";
import type { RequestHandler } from "express";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne(
{
accountOwnerId: accountId

View File

@ -1,62 +0,0 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
import { EPOCH, unixTimesInMs } from "../../constants/timeConstants.ts";
import type { ISeasonChallenge } from "../../types/worldStateTypes.ts";
import { ExportChallenges } from "warframe-public-export-plus";
export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress");
const worldState = getWorldState(undefined);
if (worldState.SeasonInfo) {
const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag);
const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong);
const nightwaveSeason = worldState.SeasonInfo.Season;
const timeMs = worldState.Time * 1000;
const completedChallengesIds = new Set<string>();
inventory.SeasonChallengeHistory.forEach(challengeHistory => {
const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1;
if (nightwaveSeason == entryNightwaveSeason) {
const meta = Object.entries(ExportChallenges).find(
([key]) => key.split("/").pop() === challengeHistory.challenge
);
if (meta) {
const [, challengeMeta] = meta;
const challengeProgress = inventory.ChallengeProgress.find(
c => c.Name === challengeHistory.challenge
);
if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) {
completedChallengesIds.add(challengeHistory.id);
}
}
}
});
const PastWeeklyChallenges: ISeasonChallenge[] = [];
let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1;
while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) {
const tempActs: ISeasonChallenge[] = [];
pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason);
for (const act of tempActs) {
if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) {
if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) {
act.Permanent = true;
}
PastWeeklyChallenges.push(act);
}
}
week--;
}
res.json({ PastWeeklyChallenges: PastWeeklyChallenges });
}
};

View File

@ -1,33 +1,40 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { IGetShipResponse, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
import { getLoadout } from "../../services/loadoutService.ts";
import { toOid } from "../../helpers/inventoryHelpers.ts";
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService";
export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRoomsDb = await getPersonalRooms(accountId);
// 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 personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId,
LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
Ship: {
...personalRooms.Ship,
ShipId: toOid(personalRoomsDb.activeShipId)
ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem
}
},
Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop
};
if (config.unlockAllShipFeatures) {
getShipResponse.Ship.Features = allShipFeatures;
}
res.json(getShipResponse);
};

View File

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

View File

@ -1,9 +1,9 @@
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { crackRelic } from "../../helpers/relicHelper.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import type { IVoidTearParticipantInfo } from "../../types/requestTypes.ts";
import type { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { crackRelic } from "@/src/helpers/relicHelper";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IVoidTearParticipantInfo } from "@/src/types/requestTypes";
import { RequestHandler } from "express";
export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -11,11 +11,7 @@ export const getVoidProjectionRewardsController: RequestHandler = async (req, re
if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
const inventory = await getInventory(accountId);
const reward = await crackRelic(inventory, data.ParticipantInfo);
if (!inventory.MissionRelicRewards || inventory.MissionRelicRewards.length >= data.CurrentWave) {
inventory.MissionRelicRewards = [];
}
inventory.MissionRelicRewards.push({ ItemType: reward.type, ItemCount: reward.itemCount });
await crackRelic(inventory, data.ParticipantInfo);
await inventory.save();
}

View File

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

View File

@ -1,14 +1,19 @@
import type { RequestHandler } from "express";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
import type { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import { EquipmentFeatures } from "../../types/equipmentTypes.ts";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards"
];
interface IGildWeaponRequest {
ItemName: string;
@ -16,14 +21,17 @@ interface IGildWeaponRequest {
PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity;
ItemId: string;
Category: TEquipmentKey;
Category: WeaponTypeInternal | "Hoverboards";
}
export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
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 weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
@ -34,10 +42,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const weapon = inventory[data.Category][weaponIndex];
weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED;
if (data.Recipe != "webui") {
weapon.ItemName = data.ItemName;
weapon.XP = 0;
}
weapon.ItemName = data.ItemName;
weapon.XP = 0;
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
weapon.Polarity = [
{
@ -50,24 +56,21 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
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
});
}
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();
@ -75,5 +78,4 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods
});
broadcastInventoryUpdate(req);
};

View File

@ -1,17 +1,16 @@
import type { RequestHandler } from "express";
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
import { getInventory } from "../../services/inventoryService.ts";
import { giveKeyChainItem } from "../../services/questService.ts";
import type { IKeyChainRequest } from "../../types/requestTypes.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts";
import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { giveKeyChainItem } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const accountId = parseString(req.query.accountId);
const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString());
const inventory = await getInventory(accountId);
const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain)!;
const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo, questKey);
const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo);
await inventory.save();
res.send(inventoryChanges);

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