Compare commits

..

4 Commits

Author SHA1 Message Date
67ee1d147b ensure that leaderboard-displayName pair is unique
Some checks failed
Build / build (22) (push) Waiting to run
Build / build (20) (push) Has been cancelled
Build / build (18) (push) Has been cancelled
Build / build (20) (pull_request) Successful in 46s
Build / build (18) (pull_request) Successful in 1m15s
Build / build (22) (pull_request) Successful in 1m23s
2025-03-24 14:31:43 +01:00
ea201e3be8 fix _id field in response
All checks were successful
Build / build (18) (push) Successful in 48s
Build / build (20) (push) Successful in 1m12s
Build / build (22) (push) Successful in 53s
Build / build (18) (pull_request) Successful in 1m14s
Build / build (22) (pull_request) Successful in 45s
Build / build (20) (pull_request) Successful in 1m21s
2025-03-24 14:31:43 +01:00
1109d8e0ff maintain logging for leaderboard request body for now
All checks were successful
Build / build (20) (push) Successful in 44s
Build / build (18) (push) Successful in 1m13s
Build / build (22) (push) Successful in 1m13s
Build / build (18) (pull_request) Successful in 46s
Build / build (20) (pull_request) Successful in 1m12s
Build / build (22) (pull_request) Successful in 1m15s
2025-03-24 14:03:26 +01:00
cc3880816a feat: daily race leaderboards
All checks were successful
Build / build (20) (push) Successful in 1m11s
Build / build (18) (push) Successful in 1m22s
Build / build (22) (push) Successful in 1m11s
Build / build (20) (pull_request) Successful in 41s
Build / build (22) (pull_request) Successful in 1m11s
Build / build (18) (pull_request) Successful in 1m18s
2025-03-24 12:53:09 +01:00
332 changed files with 16041 additions and 31524 deletions

View File

@ -1,46 +1,37 @@
{ {
"plugins": ["@typescript-eslint", "prettier", "import"],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@typescript-eslint/recommended-requiring-type-checking"
"plugin:import/recommended",
"plugin:import/typescript"
], ],
"plugins": ["@typescript-eslint", "prettier"],
"env": { "env": {
"browser": true, "browser": true,
"es6": true, "es6": true,
"node": true "node": true
}, },
"rules": { "rules": {
"@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/explicit-function-return-type": "warn",
"@typescript-eslint/restrict-template-expressions": "error", "@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/restrict-plus-operands": "error", "@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/no-unsafe-member-access": "error", "@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }], "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "error", "@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-assignment": "error", "@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "warn",
"no-loss-of-precision": "error", "@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-unnecessary-condition": "error", "@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-base-to-string": "off",
"no-case-declarations": "error", "no-case-declarations": "error",
"prettier/prettier": "error", "prettier/prettier": "error",
"@typescript-eslint/semi": "error",
"no-mixed-spaces-and-tabs": "error", "no-mixed-spaces-and-tabs": "error",
"@typescript-eslint/require-await": "error", "require-await": "off",
"import/no-named-as-default-member": "off", "@typescript-eslint/require-await": "error"
"import/no-cycle": "warn"
}, },
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "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 # Auto detect text files and perform LF normalization
* text=auto eol=lf * text=auto
static/webui/libs/ linguist-vendored static/webui/libs/ linguist-vendored

View File

@ -1,30 +1,21 @@
name: Build name: Build
on: on:
push: push: {}
branches: ["main"]
pull_request: {} pull_request: {}
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
version: [18, 20, 22]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.2 uses: actions/checkout@v4.1.2
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with: with:
node-version: ">=20.6.0" node-version: ${{ matrix.version }}
- run: npm ci - run: npm ci
- run: cp config-vanilla.json config.json - run: cp config.json.example config.json
- run: npm run verify - run: npm run build
- run: npm run lint:ci - run: npm run lint
- run: npm run prettier
- run: npm run update-translations
- run: npm run fix-imports
- name: Fail if there are uncommitted changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Uncommitted changes detected:"
git status
git --no-pager diff
exit 1
fi

View File

@ -4,9 +4,9 @@ on:
branches: branches:
- main - main
jobs: jobs:
docker-amd64: docker:
if: github.repository == 'OpenWF/SpaceNinjaServer' if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: amd64 runs-on: ubuntu-latest
steps: steps:
- name: Set up Docker buildx - name: Set up Docker buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@ -18,27 +18,8 @@ jobs:
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
platforms: linux/amd64 platforms: linux/amd64,linux/arm64
push: true push: true
tags: | tags: |
openwf/spaceninjaserver:latest openwf/spaceninjaserver:latest
openwf/spaceninjaserver:${{ github.sha }} openwf/spaceninjaserver:${{ github.sha }}
docker-arm64:
if: github.repository == 'OpenWF/SpaceNinjaServer'
runs-on: arm64
steps:
- name: Set up Docker buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
uses: docker/login-action@v3
with:
username: openwf
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/arm64
push: true
tags: |
openwf/spaceninjaserver:latest-arm64
openwf/spaceninjaserver:${{ github.sha }}-arm64

View File

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

View File

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

3
.vscode/launch.json vendored
View File

@ -8,7 +8,8 @@
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Debug and Watch", "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" "console": "integratedTerminal"
} }
] ]

View File

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

View File

@ -1,12 +1,28 @@
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_UNLOCK_ALL_QUESTS=true
ENV APP_COMPLETE_ALL_QUESTS=true
ENV APP_INFINITE_RESOURCES=true
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true
ENV APP_UNLOCK_ALL_SKINS=true
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true
ENV APP_SPOOF_MASTERY_RANK=-1
RUN apk add --no-cache bash sed wget jq
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN npm i --omit=dev
RUN npm run build
RUN date '+%d %B %Y' > BUILD_DATE
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -10,29 +10,5 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json ## 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.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
- `RadioLegionIntermission4Syndicate` for Nora's Choice
- `RadioLegionIntermission3Syndicate` for Intermission III
- `RadioLegion3Syndicate` for Glassmaker
- `RadioLegionIntermission2Syndicate` for Intermission II
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.

View File

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

View File

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

View File

@ -1,95 +0,0 @@
{
"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,
"claimingBlueprintRefundsIngredients": false,
"dontSubtractPurchaseCreditCost": false,
"dontSubtractPurchasePlatinumCost": false,
"dontSubtractPurchaseItemCost": false,
"dontSubtractPurchaseStandingCost": false,
"dontSubtractVoidTraces": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
"unlockAllSkins": false,
"unlockAllCapturaScenes": false,
"universalPolarityEverywhere": false,
"unlockDoubleCapacityPotatoesEverywhere": false,
"unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false,
"noDailyStandingLimits": false,
"noDailyFocusLimit": false,
"noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": false,
"noDeathMarks": false,
"noKimCooldowns": false,
"fullyStockedVendors": false,
"baroAlwaysAvailable": false,
"baroFullyStocked": false,
"syndicateMissionsRepeatable": false,
"unlockAllProfitTakerStages": false,
"instantFinishRivenChallenge": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"missionsCanGiveAllRelics": false,
"unlockAllSimarisResearchEntries": false,
"disableDailyTribute": false,
"spoofMasteryRank": -1,
"relicRewardItemCountMultiplier": 1,
"nightwaveStandingMultiplier": 1,
"unfaithfulBugFixes": {
"ignore1999LastRegionPlayed": false,
"fixXtraCheeseTimer": false
},
"worldState": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"tennoLiveRelay": false,
"wolfHunt": false,
"longShadow": false,
"hallowedFlame": false,
"hallowedNightmares": false,
"hallowedNightmaresRewardsOverride": 0,
"proxyRebellion": false,
"proxyRebellionRewardsOverride": 0,
"galleonOfGhouls": 0,
"ghoulEmergenceOverride": null,
"plagueStarOverride": null,
"starDaysOverride": null,
"dogDaysOverride": null,
"dogDaysRewardsOverride": null,
"eidolonOverride": "",
"vallisOverride": "",
"duviriOverride": "",
"nightwaveOverride": "",
"allTheFissures": "",
"circuitGameModes": null,
"darvoStockMultiplier": 1,
"varziaOverride": "",
"varziaFullyStocked": false
},
"dev": {
"keepVendorsExpired": false
}
}

47
config.json.example Normal file
View File

@ -0,0 +1,47 @@
{
"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,
"noVendorPurchaseLimits": true,
"instantResourceExtractorDrones": false,
"noDojoRoomBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"events": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true
}
}

View File

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

View File

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

3740
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,56 +4,41 @@
"description": "WF Emulator", "description": "WF Emulator",
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "start": "node --import ./build/src/pathman.js build/src/index.js",
"build": "tsgo --sourceMap && ncp static/webui build/static/webui", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "build": "tsc && copyfiles static/webui/** build",
"build:dev": "tsgo --sourceMap",
"build:dev:tsc": "tsc --incremental --sourceMap",
"build-and-start": "npm run build && npm run start",
"build-and-start:bun": "npm run verify && npm run bun-run",
"dev": "node scripts/dev.js",
"dev:bun": "bun scripts/dev.js",
"verify": "tsgo --noEmit",
"verify:tsc": "tsc --noEmit",
"bun-run": "bun src/index.ts",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .", "prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js", "update-translations": "cd scripts && node update-translations.js"
"fix-imports": "cd scripts && node fix-imports.js",
"fix": "npm run update-translations && npm run fix-imports && npm run prettier"
}, },
"license": "GNU", "license": "GNU",
"dependencies": { "dependencies": {
"@types/express": "^5", "@types/express": "^5",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10", "copyfiles": "^2.4.1",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "^7.0.0-dev.20250625.1",
"chokidar": "^4.0.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.4.4", "json-with-bigint": "^3.2.1",
"mongoose": "^8.11.0", "mongoose": "^8.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "typescript": ">=5.5 <5.6.0",
"typescript": "^5.5", "warframe-public-export-plus": "^0.5.48",
"undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.80",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0"
"ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^7.18",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^7.18",
"eslint": "^8", "eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-import": "^2.32.0", "prettier": "^3.4.2",
"eslint-plugin-prettier": "^5.2.5", "ts-node-dev": "^2.0.0",
"prettier": "^3.5.3", "tsconfig-paths": "^4.2.0"
"tree-kill": "^1.2.2" },
"engines": {
"node": ">=18.15.0",
"npm": ">=9.5.0"
} }
} }

View File

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

View File

@ -1,46 +0,0 @@
/* eslint-disable */
const fs = require("fs");
const path = require("path");
const root = path.join(process.cwd(), "..");
function listFiles(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
let results = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
results = results.concat(listFiles(fullPath));
} else {
results.push(fullPath);
}
}
return results;
}
const files = listFiles(path.join(root, "src"));
for (const file of files) {
let content;
try {
content = fs.readFileSync(file, "utf8");
} catch (e) {
continue;
}
const dir = path.dirname(file);
const fixedContent = content.replaceAll(/} from "([^"]+)";/g, (sub, importPath) => {
if (!importPath.startsWith("@/")) {
const fullImportPath = path.resolve(dir, importPath);
if (fs.existsSync(fullImportPath + ".ts")) {
const relative = path.relative(root, fullImportPath).replace(/\\/g, "/");
const fixedPath = "@/" + relative;
console.log(`${importPath} -> ${fixedPath}`);
return sub.split(importPath).join(fixedPath);
}
}
return sub;
});
if (content != fixedContent) {
fs.writeFileSync(file, fixedContent, "utf8");
}
}

View File

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

View File

@ -16,24 +16,17 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express(); const app = express();
app.use((req, _res, next) => { 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). // 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. // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { if (req.headers["content-encoding"] == "ezip") {
req.headers["content-encoding"] = undefined; req.headers["content-encoding"] = undefined;
} }
// U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
// U17 sets no Content-Type at all, which Express also doesn't like.
if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
req.headers["content-type"] = "application/octet-stream";
}
next(); next();
}); });
app.use(bodyParser.raw()); app.use(bodyParser.raw());
app.use(express.json({ limit: "4mb" })); app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text({ limit: "4mb" })); app.use(bodyParser.text());
app.use(requestLogger); app.use(requestLogger);
app.use("/api", apiRouter); app.use("/api", apiRouter);

View File

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

View File

@ -32,7 +32,7 @@ export const abortDojoComponentController: RequestHandler = async (req, res) =>
if (request.DecoId) { if (request.DecoId) {
removeDojoDeco(guild, request.ComponentId, request.DecoId); removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else { } else {
await removeDojoRoom(guild, request.ComponentId); removeDojoRoom(guild, request.ComponentId);
} }
await guild.save(); await guild.save();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,22 +0,0 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { 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,9 +1,9 @@
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IOid } from "@/src/types/commonTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus"; import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
@ -24,11 +24,11 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
]); ]);
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
}); });
const rawRivenType = getRandomRawRivenType(); const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!; const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]); const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex = const upgradeIndex =
@ -53,38 +53,17 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
RARE: 0, RARE: 0,
LEGENDARY: 0 LEGENDARY: 0
}; };
let forcedPolarity: string | undefined;
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType]; const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount; counts[meta.rarity] += upgrade.ItemCount;
if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
} else {
addMods(inventory, [ addMods(inventory, [
{ {
ItemType: upgrade.ItemType, ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1 ItemCount: upgrade.ItemCount * -1
} }
]); ]);
}
if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
forcedPolarity = "AP_ATTACK";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
forcedPolarity = "AP_DEFENSE";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/TacticTransmuteCore") {
forcedPolarity = "AP_TACTIC";
}
}); });
let newModType: string | undefined;
for (const specialModSet of specialModSets) {
if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
newModType = getRandomElement(specialModSet);
break;
}
}
if (!newModType) {
// Based on the table on https://wiki.warframe.com/w/Transmutation // Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = { const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4, COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
@ -95,14 +74,12 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
const options: { uniqueName: string; rarity: TRarity }[] = []; const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => { Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) { if (upgrade.canBeTransmutation) {
options.push({ uniqueName, rarity: upgrade.rarity }); options.push({ uniqueName, rarity: upgrade.rarity });
} }
}); });
newModType = getRandomWeightedReward(options, weights)!.uniqueName; const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
}
addMods(inventory, [ addMods(inventory, [
{ {
ItemType: newModType, ItemType: newModType,
@ -128,41 +105,20 @@ const getRandomRawRivenType = (): string => {
}; };
interface IArtifactTransmutationRequest { interface IArtifactTransmutationRequest {
Upgrade: IUpgradeFromClient; Upgrade: IAgnosticUpgradeClient;
LevelDiff: number; LevelDiff: number;
Consumed: IUpgradeFromClient[]; Consumed: IAgnosticUpgradeClient[];
Cost: number; Cost: number;
FusionPointCost: number; FusionPointCost: number;
RivenTransmute?: boolean; RivenTransmute?: boolean;
} }
const specialModSets: string[][] = [ interface IAgnosticUpgradeClient {
[ ItemType: string;
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", ItemId: IOid;
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", FromSKU: boolean;
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", UpgradeFingerprint: string;
"/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod", PendingRerollFingerprint: string;
"/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod", ItemCount: number;
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod", LastAdded: IOid;
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod", }
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
],
[
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
],
[
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
]
];

View File

@ -3,6 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { IInventoryClient, IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { addMods, getInventory } from "@/src/services/inventoryService"; import { addMods, getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
export const artifactsController: RequestHandler = async (req, res) => { export const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -23,6 +24,7 @@ export const artifactsController: RequestHandler = async (req, res) => {
if (itemIndex !== -1) { if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint; Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
inventory.markModified(`Upgrades.${itemIndex}.UpgradeFingerprint`);
} else { } else {
itemIndex = itemIndex =
Upgrades.push({ Upgrades.push({
@ -33,10 +35,10 @@ export const artifactsController: RequestHandler = async (req, res) => {
addMods(inventory, [{ ItemType, ItemCount: -1 }]); addMods(inventory, [{ ItemType, ItemCount: -1 }]);
} }
if (!inventory.infiniteCredits) { if (!config.infiniteCredits) {
inventory.RegularCredits -= Cost; inventory.RegularCredits -= Cost;
} }
if (!inventory.infiniteEndo) { if (!config.infiniteEndo) {
inventory.FusionPoints -= FusionPointCost; inventory.FusionPoints -= FusionPointCost;
} }

View File

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

View File

@ -15,12 +15,6 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
return; return;
} }
// Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"}
if (req.body) {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId
}
const idToNode: Record<string, INode> = {}; const idToNode: Record<string, INode> = {};
guild.DojoComponents.forEach(x => { guild.DojoComponents.forEach(x => {
idToNode[x._id.toString()] = { idToNode[x._id.toString()] = {
@ -49,13 +43,23 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
newRoot.component.pp = undefined; newRoot.component.pp = undefined;
newRoot.parent = undefined; newRoot.parent = undefined;
// Set/update SortId in top-to-bottom order // Don't even ask me why this is needed because I don't know either
const stack: INode[] = [newRoot]; const stack: INode[] = [newRoot];
let i = 0;
const idMap: Record<string, Types.ObjectId> = {};
while (stack.length != 0) { while (stack.length != 0) {
const top = stack.shift()!; const top = stack.shift()!;
top.component.SortId = new Types.ObjectId(); idMap[top.component._id.toString()] = new Types.ObjectId(
(++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
);
top.children.forEach(x => stack.push(x)); top.children.forEach(x => stack.push(x));
} }
guild.DojoComponents.forEach(x => {
x._id = idMap[x._id.toString()];
if (x.pi) {
x.pi = idMap[x.pi.toString()];
}
});
logger.debug("New tree:\n" + treeToString(newRoot)); logger.debug("New tree:\n" + treeToString(newRoot));

View File

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

View File

@ -4,36 +4,31 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
getInventory, getInventory,
updateCurrency, updateCurrency,
addItem, addItem,
addMiscItems,
addRecipes, addRecipes,
occupySlot, occupySlot
combineInventoryChanges,
addKubrowPetPrint,
addPowerSuit,
addEquipment
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
import { EquipmentFeatures, IEquipmentClient, Status } from "@/src/types/equipmentTypes";
interface IClaimCompletedRecipeRequest { export interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
} }
export const claimCompletedRecipeController: RequestHandler = async (req, res) => { export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body)); const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString()); if (!accountId) throw new Error("no account id");
const inventory = await getInventory(accountId);
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid); const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) { if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
@ -52,14 +47,39 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
if (req.query.cancel) { if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); ...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const nonMiscItemIngredients = 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>());
nonMiscItemIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
const miscItemChanges: IMiscItem[] = [];
recipe.ingredients.forEach(ingredient => {
if (!nonMiscItemIngredients.has(ingredient.ItemType)) {
miscItemChanges.push(ingredient);
}
});
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
await inventory.save(); await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOidWithLegacySupport[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= []; inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= []; inventory.SpectreLoadouts ??= [];
@ -79,15 +99,9 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
} }
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
} }
let InventoryChanges: IInventoryChanges = {}; let InventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
addRecipes(inventory, [ addRecipes(inventory, [
{ {
@ -97,192 +111,16 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
]); ]);
} }
if (req.query.rush) { if (req.query.rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...updateCurrency(inventory, cost, true) ...updateCurrency(inventory, recipe.skipBuildTimePrice, 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();
}
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, 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];
InventoryChanges.Upgrades ??= [];
InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
await addPowerSuit(
inventory,
"/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
{
Configs: [
{
Upgrades: [
"",
"",
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
]
}
],
XP: 900_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
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
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
XP: 450_000
});
} else {
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await addItem( ...(await addItem(inventory, recipe.resultType, recipe.num, false))
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
}; };
}
}
if (
config.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
}
await inventory.save(); await inventory.save();
res.json({ InventoryChanges, BrandedSuits }); res.json({ InventoryChanges });
}
};
const refundRecipeIngredients = async (
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase
): Promise<void> => {
updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
} }
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,9 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService"; import { config } from "@/src/services/configService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService"; import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
@ -10,7 +11,7 @@ import { Types } from "mongoose";
export const contributeGuildClassController: RequestHandler = async (req, res) => { export const contributeGuildClassController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<IContributeGuildClassRequest>(String(req.body)); const payload = getJSONfromString<IContributeGuildClassRequest>(String(req.body));
const guild = (await Guild.findById(payload.GuildId))!; const guild = (await Guild.findOne({ _id: payload.GuildId }))!;
// First contributor initiates ceremony and locks the pending class. // First contributor initiates ceremony and locks the pending class.
if (!guild.CeremonyContributors) { if (!guild.CeremonyContributors) {
@ -30,13 +31,49 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
guild.CeremonyContributors.push(new Types.ObjectId(accountId)); 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.toString(), [
{
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(); await guild.save();
// Either way, endo is given to the contributor. // Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints"); const inventory = await getInventory(accountId, "FusionPoints");
addFusionPoints(inventory, guild.CeremonyEndo!); inventory.FusionPoints += guild.CeremonyEndo!;
await inventory.save(); await inventory.save();
res.json({ res.json({

View File

@ -1,7 +1,6 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { import {
addGuildMemberMiscItemContribution,
getDojoClient, getDojoClient,
getGuildForRequestEx, getGuildForRequestEx,
hasAccessToDojo, hasAccessToDojo,
@ -11,7 +10,7 @@ import {
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IDojoContributable, IGuildMemberDatabase } from "@/src/types/guildTypes"; import { IDojoContributable } from "@/src/types/guildTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -36,10 +35,6 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
return; return;
} }
const guild = await getGuildForRequestEx(req, inventory); const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest; const request = JSON.parse(String(req.body)) as IContributeToDojoComponentRequest;
const component = guild.DojoComponents.id(request.ComponentId)!; const component = guild.DojoComponents.id(request.ComponentId)!;
@ -50,7 +45,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
throw new Error("attempt to contribute to a deco in an unfinished room?!"); throw new Error("attempt to contribute to a deco in an unfinished room?!");
} }
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, component); processContribution(guild, request, inventory, inventoryChanges, meta, component);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (component.CompletionTime) { if (component.CompletionTime) {
setDojoRoomLogFunded(guild, component); setDojoRoomLogFunded(guild, component);
@ -60,11 +55,12 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
if (request.DecoId) { if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(guild, guildMember, request, inventory, inventoryChanges, meta, deco); processContribution(guild, request, inventory, inventoryChanges, meta, deco);
} }
} }
await Promise.all([guild.save(), inventory.save(), guildMember.save()]); await guild.save();
await inventory.save();
res.json({ res.json({
...(await getDojoClient(guild, 0, component._id)), ...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges
@ -73,7 +69,6 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
const processContribution = ( const processContribution = (
guild: TGuildDatabaseDocument, guild: TGuildDatabaseDocument,
guildMember: IGuildMemberDatabase,
request: IContributeToDojoComponentRequest, request: IContributeToDojoComponentRequest,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges, inventoryChanges: IInventoryChanges,
@ -85,18 +80,15 @@ const processContribution = (
component.RegularCredits += request.RegularCredits; component.RegularCredits += request.RegularCredits;
inventoryChanges.RegularCredits = -request.RegularCredits; inventoryChanges.RegularCredits = -request.RegularCredits;
updateCurrency(inventory, request.RegularCredits, false); updateCurrency(inventory, request.RegularCredits, false);
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += request.RegularCredits;
} }
if (request.VaultCredits) { if (request.VaultCredits) {
component.RegularCredits += request.VaultCredits; component.RegularCredits += request.VaultCredits;
guild.VaultRegularCredits! -= request.VaultCredits; guild.VaultRegularCredits! -= request.VaultCredits;
} }
if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) { if (component.RegularCredits > scaleRequiredCount(meta.price)) {
guild.VaultRegularCredits ??= 0; guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price); guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price);
component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price); component.RegularCredits = scaleRequiredCount(meta.price);
} }
component.MiscItems ??= []; component.MiscItems ??= [];
@ -107,10 +99,10 @@ const processContribution = (
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if ( if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount > componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) scaleRequiredCount(ingredientMeta.ItemCount)
) { ) {
ingredientContribution.ItemCount = ingredientContribution.ItemCount =
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
} }
componentMiscItem.ItemCount += ingredientContribution.ItemCount; componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else { } else {
@ -128,10 +120,10 @@ const processContribution = (
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if ( if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount > componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) scaleRequiredCount(ingredientMeta.ItemCount)
) { ) {
ingredientContribution.ItemCount = ingredientContribution.ItemCount =
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount; scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
} }
componentMiscItem.ItemCount += ingredientContribution.ItemCount; componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else { } else {
@ -141,21 +133,16 @@ const processContribution = (
ItemType: ingredientContribution.ItemType, ItemType: ingredientContribution.ItemType,
ItemCount: ingredientContribution.ItemCount * -1 ItemCount: ingredientContribution.ItemCount * -1
}); });
addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges; inventoryChanges.MiscItems = miscItemChanges;
} }
if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { if (component.RegularCredits >= scaleRequiredCount(meta.price)) {
let fullyFunded = true; let fullyFunded = true;
for (const ingredient of meta.ingredients) { for (const ingredient of meta.ingredients) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType);
if ( if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) {
!componentMiscItem ||
componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
fullyFunded = false; fullyFunded = false;
break; break;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,54 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers 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,106 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { ICrewShipComponentFingerprint, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
export const crewShipFusionController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
const inventoryChanges: IInventoryChanges = {};
// Charge partial repair cost if fusing with an identified but unrepaired part
if (payload.SourceRecipe) {
const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
// Remove inferior item
if (payload.SourceRecipe) {
inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
} else {
const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
inventory[category].pull({ _id: inferiorId.$oid });
inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
}
// Upgrade superior item
const superiorItem = tierA < tierB ? itemB : itemA;
const inferiorItem = tierA < tierB ? itemA : itemB;
const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
superiorItem.UpgradeFingerprint!
) as ICrewShipComponentFingerprint;
const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
: { compat: "", buffs: [] };
if (isWeapon) {
for (let i = 0; i != fingerprint.buffs.length; ++i) {
const buffA = fingerprint.buffs[i];
const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
const fvalA = buffA.Value / 0x3fffffff;
const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
const percA = 0.3 + fvalA * (0.6 - 0.3);
const percB = 0.3 + fvalB * (0.6 - 0.3);
const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
const newFval = (newPerc - 0.3) / (0.6 - 0.3);
buffA.Value = Math.trunc(newFval * 0x3fffffff);
}
} else {
const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
const buffA = fingerprint.buffs[i];
const buffB = inferiorFingerprint.buffs[i];
const fvalA = buffA.Value / 0x3fffffff;
const fvalB = buffB.Value / 0x3fffffff;
const rangeA = superiorMeta[i].range;
const rangeB = inferiorMeta[i].range;
const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
buffA.Value = Math.trunc(newFval * 0x3fffffff);
}
if (inferiorFingerprint.SubroutineIndex) {
const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
if (!useSuperiorSubroutine) {
fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
}
}
}
superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
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,84 +0,0 @@
import {
addCrewShipSalvagedWeaponSkin,
addCrewShipRawSalvage,
getInventory,
addEquipment
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/equipmentTypes";
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,64 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
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 component = guild.DojoComponents.id(data.c)!;
if (req.query.act == "f") {
res.json({
results: component.Leaderboard ?? []
});
} else if (req.query.act == "p") {
const account = await getAccountForRequest(req);
component.Leaderboard ??= [];
const entry = component.Leaderboard.find(x => x.n == account.DisplayName);
if (entry) {
entry.s = data.s!;
} else {
component.Leaderboard.push({
s: data.s!,
n: account.DisplayName,
r: 0
});
}
component.Leaderboard.sort((a, b) => a.s - b.s); // In this case, the score is the time in milliseconds, so smaller is better.
if (component.Leaderboard.length > 10) {
component.Leaderboard.shift();
}
let r = 0;
for (const entry of component.Leaderboard) {
entry.r = ++r;
}
await guild.save();
res.status(200).end();
} else 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)}`);
throw new Error(`unknown customObstacleCourseLeaderboard act: ${String(req.query.act)}`);
}
};
interface ICustomObstacleCourseLeaderboardRequest {
g: string;
c: string;
s?: number; // act=p
}

View File

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

View File

@ -3,13 +3,11 @@ import {
getGuildForRequestEx, getGuildForRequestEx,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
refundDojoDeco,
removeDojoDeco removeDojoDeco
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => { export const destroyDojoDecoController: RequestHandler = async (req, res) => {
@ -20,20 +18,9 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest; const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
if ("DecoType" in request) {
removeDojoDeco(guild, request.ComponentId, request.DecoId); 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);
}
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId)); res.json(await getDojoClient(guild, 0, request.ComponentId));
@ -44,8 +31,3 @@ interface IDestroyDojoDecoRequest {
ComponentId: string; ComponentId: string;
DecoId: string; DecoId: string;
} }
interface IClearObstacleCourseRequest {
ComponentId: string;
Act: "cObst" | "maybesomethingelsewedontknowabout";
}

View File

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

View File

@ -1,4 +1,3 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
@ -36,10 +35,10 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
if (request.DecoId) { if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(guild, deco, meta, platinumDonated); processContribution(deco, meta, platinumDonated);
} else { } else {
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(guild, component, meta, platinumDonated); processContribution(component, meta, platinumDonated);
const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
if (entry) { if (entry) {
@ -47,25 +46,16 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
} }
} }
const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!; await guild.save();
guildMember.PremiumCreditsContributed ??= 0; await inventory.save();
guildMember.PremiumCreditsContributed += request.Amount;
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({ res.json({
...(await getDojoClient(guild, 0, component._id)), ...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges
}); });
}; };
const processContribution = ( const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => {
guild: TGuildDatabaseDocument, const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice);
component: IDojoContributable,
meta: IDojoBuild,
platinumDonated: number
): void => {
const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice);
const fullDurationSeconds = meta.time; const fullDurationSeconds = meta.time;
const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost;
component.CompletionTime = new Date( component.CompletionTime = new Date(

View File

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

View File

@ -55,7 +55,7 @@ export const dronesController: RequestHandler = async (req, res) => {
? new Date() ? new Date()
: new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage = drone.PendingDamage =
!config.noResourceExtractorDronesDamage && Math.random() < system.damageChance Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0; : 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!; const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;

View File

@ -1,529 +1,60 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const endlessXpController: RequestHandler = async (req, res) => { export const endlessXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IEndlessXpRequest>(String(req.body)); const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
if (payload.Mode == "r") {
const inventory = await getInventory(accountId, "EndlessXP");
inventory.EndlessXP ??= []; inventory.EndlessXP ??= [];
let entry = inventory.EndlessXP.find(x => x.Category == payload.Category); const entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (!entry) { if (entry) {
entry = { entry.Choices = payload.Choices;
} else {
inventory.EndlessXP.push({
Category: payload.Category,
Choices: payload.Choices
});
}
await inventory.save();
res.json({
NewProgress: {
Category: payload.Category, Category: payload.Category,
Earn: 0, Earn: 0,
Claim: 0, Claim: 0,
BonusAvailable: {
$date: {
$numberLong: "9999999999999"
}
},
Expiry: {
$date: {
$numberLong: "9999999999999"
}
},
Choices: payload.Choices, Choices: payload.Choices,
PendingRewards: [] 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);
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}`);
}
};
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, RequiredTotalXp: 190,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 400,
Rewards: [ Rewards: [
{ {
StoreItem: choiceRewards[0], StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod",
ItemCount: 1
}
]
},
{
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
}
]
},
{
RequiredTotalXp: 1540,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards"
)
},
{
RequiredTotalXp: 1950,
Rewards: [
{
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 ItemCount: 1
} }
] ]
} }
]; // ...
]
}
});
}; };
const hardModeChosenRewards: Record<string, string> = { interface IEndlessXpRequest {
Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker", Mode: string; // "r"
Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker", Category: TEndlessXpCategory;
Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker", Choices: string[];
Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker", }
Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
};
const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
return [
{
RequiredTotalXp: 285,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 600,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 945,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1335,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1785,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[0]],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2310,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 2925,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 3645,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 4485,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
)
},
{
RequiredTotalXp: 5460,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[1]],
ItemCount: 1
}
]
}
];
};

View File

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

View File

@ -3,7 +3,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService"; import { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService";
import { EquipmentFeatures } from "@/src/types/equipmentTypes"; import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const evolveWeaponController: RequestHandler = async (req, res) => { export const evolveWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 })) recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
); );
const item = inventory[payload.Category].id(req.query.ItemId as string)!; const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features ??= 0; item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS; item.Features |= EquipmentFeatures.INCARNON_GENESIS;
@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
} }
]); ]);
const item = inventory[payload.Category].id(req.query.ItemId as string)!; const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!;
item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS; item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
} else { } else {
throw new Error(`unexpected evolve weapon action: ${payload.Action}`); throw new Error(`unexpected evolve weapon action: ${payload.Action}`);

View File

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

View File

@ -4,6 +4,7 @@ import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/serv
import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { ExportFocusUpgrades } from "warframe-public-export-plus";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const focusController: RequestHandler = async (req, res) => { export const focusController: RequestHandler = async (req, res) => {
@ -17,8 +18,8 @@ export const focusController: RequestHandler = async (req, res) => {
case FocusOperation.InstallLens: { case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest; const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const item = inventory[request.Category].id(request.WeaponId); for (const item of inventory[request.Category]) {
if (item) { if (item._id.toString() == request.WeaponId) {
item.FocusLens = request.LensType; item.FocusLens = request.LensType;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -26,6 +27,8 @@ export const focusController: RequestHandler = async (req, res) => {
ItemCount: -1 ItemCount: -1
} satisfies IMiscItem } satisfies IMiscItem
]); ]);
break;
}
} }
await inventory.save(); await inventory.save();
res.json({ res.json({
@ -42,7 +45,7 @@ export const focusController: RequestHandler = async (req, res) => {
inventory.FocusAbility ??= focusType; inventory.FocusAbility ??= focusType;
inventory.FocusUpgrades.push({ ItemType: focusType }); inventory.FocusUpgrades.push({ ItemType: focusType });
if (inventory.FocusXP) { if (inventory.FocusXP) {
inventory.FocusXP[focusPolarity]! -= cost; inventory.FocusXP[focusPolarity] -= cost;
} }
await inventory.save(); await inventory.save();
res.json({ res.json({
@ -63,9 +66,7 @@ export const focusController: RequestHandler = async (req, res) => {
} }
); );
res.json({ res.end();
FocusUpgrade: { ItemType: focusType }
});
break; break;
} }
case FocusOperation.UnlockUpgrade: { case FocusOperation.UnlockUpgrade: {
@ -77,7 +78,7 @@ export const focusController: RequestHandler = async (req, res) => {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost; cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 }); inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
} }
inventory.FocusXP![focusPolarity]! -= cost; inventory.FocusXP![focusPolarity] -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusTypes: request.FocusTypes, FocusTypes: request.FocusTypes,
@ -95,7 +96,7 @@ export const focusController: RequestHandler = async (req, res) => {
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!; const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
focusUpgradeDb.Level = focusUpgrade.Level; focusUpgradeDb.Level = focusUpgrade.Level;
} }
inventory.FocusXP![focusPolarity]! -= cost; inventory.FocusXP![focusPolarity] -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusInfos: request.FocusInfos, FocusInfos: request.FocusInfos,
@ -105,24 +106,23 @@ export const focusController: RequestHandler = async (req, res) => {
} }
case FocusOperation.SentTrainingAmplifier: { case FocusOperation.SentTrainingAmplifier: {
const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest; const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
const inventory = await getInventory(accountId); const parts: string[] = [
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip", "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis", "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel" "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
] ];
}); const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
occupySlot(inventory, InventorySlot.AMPS, false); occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save(); await inventory.save();
res.json(inventoryChanges.OperatorAmps![0]); res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);
break; break;
} }
case FocusOperation.UnbindUpgrade: { case FocusOperation.UnbindUpgrade: {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest; const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length; inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
@ -167,10 +167,8 @@ export const focusController: RequestHandler = async (req, res) => {
shard.ItemCount *= -1; shard.ItemCount *= -1;
} }
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const polarity = request.Polarity; inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 };
inventory.FocusXP ??= {}; inventory.FocusXP[request.Polarity] += xp;
inventory.FocusXP[polarity] ??= 0;
inventory.FocusXP[polarity] += xp;
addMiscItems(inventory, request.Shards); addMiscItems(inventory, request.Shards);
await inventory.save(); await inventory.save();
break; break;

View File

@ -2,14 +2,22 @@ import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus"; import { ExportResources } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { parseFusionTreasure } from "@/src/helpers/inventoryHelpers";
interface IFusionTreasureRequest { interface IFusionTreasureRequest {
oldTreasureName: string; oldTreasureName: string;
newTreasureName: 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) => { export const fusionTreasuresController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
export const getGuildController: RequestHandler = async (req, res) => { const getGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId"); const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) { if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId); const guild = await Guild.findOne({ _id: inventory.GuildId });
if (guild) { if (guild) {
// Handle guilds created before we added discriminators // Handle guilds created before we added discriminators
if (guild.Name.indexOf("#") == -1) { if (guild.Name.indexOf("#") == -1) {
@ -24,9 +24,11 @@ export const getGuildController: RequestHandler = async (req, res) => {
guild.CeremonyResetDate = undefined; guild.CeremonyResetDate = undefined;
await guild.save(); await guild.save();
} }
res.json(await getGuildClient(guild, account)); res.json(await getGuildClient(guild, accountId));
return; return;
} }
} }
res.end(); res.sendStatus(200);
}; };
export { getGuildController };

View File

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

View File

@ -9,7 +9,7 @@ export const getGuildLogController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId"); const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) { if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId); const guild = await Guild.findOne({ _id: inventory.GuildId });
if (guild) { if (guild) {
const log: Record<string, IGuildLogEntryClient[]> = { const log: Record<string, IGuildLogEntryClient[]> = {
RoomChanges: [], RoomChanges: [],

View File

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

View File

@ -1,12 +1,13 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { generateRewardSeed } from "@/src/services/rngService"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => { export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed(); const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne( await Inventory.updateOne(
{ {
accountOwnerId: accountId accountOwnerId: accountId
@ -17,3 +18,9 @@ export const getNewRewardSeedController: RequestHandler = async (req, res) => {
); );
res.json({ rewardSeed: rewardSeed }); res.json({ rewardSeed: rewardSeed });
}; };
export function generateRewardSeed(): number {
const min = -Number.MAX_SAFE_INTEGER;
const max = Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@ -1,62 +0,0 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "@/src/services/worldStateService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { ISeasonChallenge } from "@/src/types/worldStateTypes";
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

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

View File

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

View File

@ -1,125 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account } from "@/src/models/loginModel";
import { areFriends } from "@/src/services/friendService";
import { createMessage } from "@/src/services/inboxService";
import {
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleDailyDealPurchase, handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes";
import { IPurchaseParams, IPurchaseResponse, PurchaseSource } from "@/src/types/purchaseTypes";
import { 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;
};
export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body));
if (!checkPurchaseParams(data.PurchaseParams)) {
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
}
const account = await Account.findOne(
data.RecipientId ? { _id: data.RecipientId.$oid } : { DisplayName: data.Recipient }
);
if (!account) {
res.status(400).send("9").end();
return;
}
const inventory = await getInventory(account._id.toString(), "Suits Settings");
// Cannot gift items to players that have not completed the tutorial.
if (inventory.Suits.length == 0) {
res.status(400).send("14").end();
return;
}
// Cannot gift to players who have gifting disabled.
const senderAccount = await getAccountForRequest(req);
if (
inventory.Settings?.GiftMode == "GIFT_MODE_NONE" ||
(inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).send("17").end();
return;
}
// TODO: Cannot gift items with mastery requirement to players who are too low level. (Code 2)
// TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7)
// TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20)
const senderInventory = await getInventory(senderAccount._id.toString());
if (senderInventory.GiftsRemaining == 0) {
res.status(400).send("10").end();
return;
}
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
);
}
}
await senderInventory.save();
const senderName = getSuffixedName(senderAccount);
await createMessage(account._id, [
{
sndr: senderName,
msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage",
arg: [
{
Key: "GIFTER_NAME",
Tag: senderName
},
{
Key: "GIFT_QUANTITY",
Tag: data.PurchaseParams.Quantity
}
],
sub: "/Lotus/Language/Menu/GiftReceivedSubject",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
gifts: [
{
GiftType: data.PurchaseParams.StoreItem
}
]
}
]);
res.json(response);
};
interface IGiftingRequest {
PurchaseParams: IPurchaseParams;
Message?: string;
Recipient?: string;
RecipientId?: IOid;
buildLabel: string;
}

View File

@ -1,13 +1,19 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus"; import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures, IEquipmentClient } from "@/src/types/equipmentTypes";
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards"
];
interface IGildWeaponRequest { interface IGildWeaponRequest {
ItemName: string; ItemName: string;
@ -15,14 +21,17 @@ interface IGildWeaponRequest {
PolarizeSlot?: number; PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity; PolarizeValue?: ArtifactPolarity;
ItemId: string; ItemId: string;
Category: TEquipmentKey; Category: WeaponTypeInternal | "Hoverboards";
} }
export const gildWeaponController: RequestHandler = async (req, res) => { export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IGildWeaponRequest>(String(req.body)); const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
data.ItemId = String(req.query.ItemId); data.ItemId = String(req.query.ItemId);
data.Category = req.query.Category as TEquipmentKey; if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
}
data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId); const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
@ -33,10 +42,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const weapon = inventory[data.Category][weaponIndex]; const weapon = inventory[data.Category][weaponIndex];
weapon.Features ??= 0; weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED; weapon.Features |= EquipmentFeatures.GILDED;
if (data.Recipe != "webui") {
weapon.ItemName = data.ItemName; weapon.ItemName = data.ItemName;
weapon.XP = 0; weapon.XP = 0;
}
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
weapon.Polarity = [ weapon.Polarity = [
{ {
@ -49,9 +56,6 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()]; inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const affiliationMods = [];
if (data.Recipe != "webui") {
const recipe = ExportRecipes[data.Recipe]; const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType, ItemType: ingredient.ItemType,
@ -59,6 +63,7 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
})); }));
addMiscItems(inventory, inventoryChanges.MiscItems); addMiscItems(inventory, inventoryChanges.MiscItems);
const affiliationMods = [];
if (recipe.syndicateStandingChange) { if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value; affiliation.Standing += recipe.syndicateStandingChange.value;
@ -67,12 +72,10 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
Standing: recipe.syndicateStandingChange.value Standing: recipe.syndicateStandingChange.value
}); });
} }
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods AffiliationMods: affiliationMods
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };

View File

@ -2,8 +2,8 @@ import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general"; import { parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { IGroup } from "@/src/types/loginTypes";
import { giveKeyChainItem } from "@/src/services/questService"; import { giveKeyChainItem } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId); const accountId = parseString(req.query.accountId);
@ -15,3 +15,9 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req,
res.send(inventoryChanges); res.send(inventoryChanges);
}; };
export interface IKeyChainRequest {
KeyChain: string;
ChainStage: number;
Groups?: IGroup[];
}

View File

@ -1,7 +1,7 @@
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { giveKeyChainMessage } from "@/src/services/questService"; import { giveKeyChainMessage } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => {

View File

@ -16,15 +16,15 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
await inventory.save(); await inventory.save();
res.json(inventoryChanges); res.json(inventoryChanges.InventoryChanges);
//TODO: consider whishlist changes //TODO: consider whishlist changes
}; };
interface IQuestKeyRewardRequest { export interface IQuestKeyRewardRequest {
reward: IQuestKeyReward; reward: IQuestKeyReward;
} }
interface IQuestKeyReward { export interface IQuestKeyReward {
RewardType: string; RewardType: string;
CouponType: string; CouponType: string;
Icon: string; Icon: string;

View File

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

View File

@ -1,8 +1,20 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addStartingGear, getInventory } from "@/src/services/inventoryService"; import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel";
import {
addEquipment,
addItem,
addPowerSuit,
combineInventoryChanges,
getInventory,
updateSlots
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { HydratedDocument } from "mongoose";
type TPartialStartingGear = Pick<IInventoryClient, "LongGuns" | "Suits" | "Pistols" | "Melee">;
export const giveStartingGearController: RequestHandler = async (req, res) => { export const giveStartingGearController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -14,3 +26,72 @@ export const giveStartingGearController: RequestHandler = async (req, res) => {
res.send(inventoryChanges); res.send(inventoryChanges);
}; };
//TODO: RawUpgrades might need to return a LastAdded
const awakeningRewards = [
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4",
"/Lotus/Types/Restoratives/LisetAutoHack",
"/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod"
];
export const addStartingGear = async (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
startingGear: TPartialStartingGear | undefined = undefined
): Promise<IInventoryChanges> => {
const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],
Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }],
Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }]
};
//TODO: properly merge weapon bin changes it is currently static here
const inventoryChanges: IInventoryChanges = {};
addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges);
addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges);
addEquipment(
inventory,
"DataKnives",
"/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon",
undefined,
inventoryChanges,
{ XP: 450_000 }
);
addEquipment(
inventory,
"Scoops",
"/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest",
undefined,
inventoryChanges
);
updateSlots(inventory, InventorySlot.SUITS, 0, 1);
updateSlots(inventory, InventorySlot.WEAPONS, 0, 3);
inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 };
inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 };
await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain");
inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain";
inventory.PremiumCredits = 50;
inventory.PremiumCreditsFree = 50;
inventoryChanges.PremiumCredits = 50;
inventoryChanges.PremiumCreditsFree = 50;
inventory.RegularCredits = 3000;
inventoryChanges.RegularCredits = 3000;
for (const item of awakeningRewards) {
const inventoryDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryDelta);
}
inventory.PlayedParkourTutorial = true;
inventory.ReceivedStartingGear = true;
return inventoryChanges;
};

View File

@ -1,47 +1,38 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { import {
addGuildMemberMiscItemContribution,
getGuildForRequestEx, getGuildForRequestEx,
getGuildVault, getGuildVault,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
processCompletedGuildTechProject,
processFundedGuildTechProject,
processGuildTechProjectContributionsUpdate,
removePigmentsFromGuildMembers, removePigmentsFromGuildMembers,
scaleRequiredCount, scaleRequiredCount
setGuildTechLogState
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus"; import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
addCrewShipWeaponSkin,
addEquipment,
addItem, addItem,
addMiscItems, addMiscItems,
addRecipes, addRecipes,
combineInventoryChanges, combineInventoryChanges,
getInventory, getInventory,
occupySlot,
updateCurrency updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes"; import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes";
import { GuildMember } from "@/src/models/guildModel"; import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const guildTechController: RequestHandler = async (req, res) => { export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest; const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") { if (data.Action == "Sync") {
let needSave = false; let needSave = false;
const techProjects: ITechProjectClient[] = []; const techProjects: ITechProjectClient[] = [];
const guild = await getGuildForRequestEx(req, inventory);
if (guild.TechProjects) { if (guild.TechProjects) {
for (const project of guild.TechProjects) { for (const project of guild.TechProjects) {
const techProject: ITechProjectClient = { const techProject: ITechProjectClient = {
@ -52,12 +43,8 @@ export const guildTechController: RequestHandler = async (req, res) => {
}; };
if (project.CompletionDate) { if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate); techProject.CompletionDate = toMongoDate(project.CompletionDate);
if ( if (Date.now() >= project.CompletionDate.getTime()) {
Date.now() >= project.CompletionDate.getTime() && needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate);
setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate)
) {
processCompletedGuildTechProject(guild, project.ItemType);
needSave = true;
} }
} }
techProjects.push(techProject); techProjects.push(techProject);
@ -68,8 +55,6 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
res.json({ TechProjects: techProjects }); res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") { } else if (data.Action == "Start") {
if (data.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -81,17 +66,17 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.TechProjects[ guild.TechProjects[
guild.TechProjects.push({ guild.TechProjects.push({
ItemType: data.RecipeType, ItemType: data.RecipeType,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price), ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price),
ReqItems: recipe.ingredients.map(x => ({ ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType, ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount) ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount)
})), })),
State: 0 State: 0
}) - 1 }) - 1
]; ];
setGuildTechLogState(guild, techProject.ItemType, 5); setTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) { if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe); processFundedProject(guild, techProject, recipe);
} else { } else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType; guild.ActiveDojoColorResearch = data.RecipeType;
@ -100,109 +85,29 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
await guild.save(); await guild.save();
res.end(); res.end();
} else {
const recipe = ExportDojoRecipes.research[data.RecipeType];
if (data.TechProductCategory) {
if (
data.TechProductCategory != "CrewShipWeapons" &&
data.TechProductCategory != "CrewShipWeaponSkins"
) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) {
throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
);
}
}
const techProject =
inventory.PersonalTechProjects[
inventory.PersonalTechProjects.push({
State: 0,
ReqCredits: recipe.price,
ItemType: data.RecipeType,
ProductCategory: data.TechProductCategory,
CategoryItemId: data.CategoryItemId,
ReqItems: recipe.ingredients
}) - 1
];
await inventory.save();
res.json({
isPersonal: true,
action: "Start",
personalTech: techProject.toJSON()
});
}
} else if (data.Action == "Contribute") { } else if (data.Action == "Contribute") {
if ((req.query.guildId as string) == "000000000000000000000000") {
const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
techProject.ReqCredits -= data.RegularCredits;
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
const miscItemChanges = [];
for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
}
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
techProject.HasContributions = true;
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
techProject.State = 1;
const recipe = ExportDojoRecipes.research[techProject.ItemType];
techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000);
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
PersonalResearch: { $oid: data.ResearchId },
PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
});
} else {
if (!hasAccessToDojo(inventory)) { if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
} }
const contributions = data;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
const guild = await getGuildForRequestEx(req, inventory); if (contributions.VaultCredits) {
const guildMember = (await GuildMember.findOne( if (contributions.VaultCredits > techProject.ReqCredits) {
{ accountId, guildId: guild._id }, contributions.VaultCredits = techProject.ReqCredits;
"RegularCreditsContributed MiscItemsContributed"
))!;
const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
if (data.VaultCredits) {
if (data.VaultCredits > techProject.ReqCredits) {
data.VaultCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= data.VaultCredits; techProject.ReqCredits -= contributions.VaultCredits;
guild.VaultRegularCredits! -= data.VaultCredits; guild.VaultRegularCredits! -= contributions.VaultCredits;
} }
if (data.RegularCredits > techProject.ReqCredits) { if (contributions.RegularCredits > techProject.ReqCredits) {
data.RegularCredits = techProject.ReqCredits; contributions.RegularCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= data.RegularCredits; techProject.ReqCredits -= contributions.RegularCredits;
guildMember.RegularCreditsContributed ??= 0; if (contributions.VaultMiscItems.length) {
guildMember.RegularCreditsContributed += data.RegularCredits; for (const miscItem of contributions.VaultMiscItems) {
if (data.VaultMiscItems.length) {
for (const miscItem of data.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -217,7 +122,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
const miscItemChanges = []; const miscItemChanges = [];
for (const miscItem of data.MiscItems) { for (const miscItem of contributions.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -228,34 +133,34 @@ export const guildTechController: RequestHandler = async (req, res) => {
ItemType: miscItem.ItemType, ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1 ItemCount: miscItem.ItemCount * -1
}); });
addGuildMemberMiscItemContribution(guildMember, miscItem);
} }
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false); const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges; inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now. if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
await processGuildTechProjectContributionsUpdate(guild, techProject); // This research is now fully funded.
const recipe = ExportDojoRecipes.research[data.RecipeType];
processFundedProject(guild, techProject, recipe);
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = "";
await removePigmentsFromGuildMembers(guild._id);
}
}
await Promise.all([guild.save(), inventory.save(), guildMember.save()]); await guild.save();
await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild) Vault: getGuildVault(guild)
}); });
}
} else if (data.Action.split(",")[0] == "Buy") { } else if (data.Action.split(",")[0] == "Buy") {
const purchase = data as IGuildTechBuyRequest; if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
if (purchase.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (
!hasAccessToDojo(inventory) ||
!(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))
) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
} }
const purchase = data as IGuildTechBuyRequest;
const quantity = parseInt(data.Action.split(",")[1]); const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [ const recipeChanges = [
{ {
@ -277,15 +182,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
Recipes: recipeChanges Recipes: recipeChanges
} }
}); });
} else {
const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
}
} else if (data.Action == "Fabricate") { } else if (data.Action == "Fabricate") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -302,7 +199,6 @@ export const guildTechController: RequestHandler = async (req, res) => {
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges }); res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") { } else if (data.Action == "Pause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -314,7 +210,6 @@ export const guildTechController: RequestHandler = async (req, res) => {
await removePigmentsFromGuildMembers(guild._id); await removePigmentsFromGuildMembers(guild._id);
res.end(); res.end();
} else if (data.Action == "Unpause") { } else if (data.Action == "Unpause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -324,140 +219,72 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.ActiveDojoColorResearch = data.RecipeType; guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save(); await guild.save();
res.end(); res.end();
} else if (data.Action == "Cancel" && data.CategoryItemId) {
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
x.CategoryItemId?.equals(data.CategoryItemId)
);
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
const contributedCredits = meta.price - personalTechProject.ReqCredits;
const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false);
inventoryChanges.MiscItems = [];
for (const ingredient of meta.ingredients) {
const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
if (reqItem) {
const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
inventoryChanges.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: contributedItems
});
}
}
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
action: "Cancel",
isPersonal: true,
inventoryChanges: inventoryChanges,
personalTech: {
ItemId: toOid(personalTechProject._id)
}
});
} else if (data.Action == "Rush" && data.CategoryItemId) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, 20, true),
...claimSalvagedComponent(inventory, data.CategoryItemId)
};
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else if (data.Action == "InstantFinish") {
if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!);
inventoryChanges.MiscItems = [
{
ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem",
ItemCount: -1
}
];
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled guildTech request`); throw new Error(`unknown guildTech action: ${data.Action}`);
} }
}; };
const processFundedProject = (
guild: TGuildDatabaseDocument,
techProject: ITechProjectDatabase,
recipe: IDojoResearch
): void => {
techProject.State = 1;
techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000);
if (recipe.guildXpValue) {
guild.XP += recipe.guildXpValue;
}
setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate);
};
const setTechLogState = (
guild: TGuildDatabaseDocument,
type: string,
state: number,
dateTime: Date | undefined = undefined
): boolean => {
guild.TechChanges ??= [];
const entry = guild.TechChanges.find(x => x.details == type);
if (entry) {
if (entry.entryType == state) {
return false;
}
entry.dateTime = dateTime;
entry.entryType = state;
} else {
guild.TechChanges.push({
dateTime: dateTime,
entryType: state,
details: type
});
}
return true;
};
type TGuildTechRequest = type TGuildTechRequest =
| { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" } | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" }
| IGuildTechBasicRequest | IGuildTechBasicRequest
| IGuildTechContributeRequest; | IGuildTechContributeRequest;
interface IGuildTechBasicRequest { interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish"; Action: "Start" | "Fabricate" | "Pause" | "Unpause";
Mode: "Guild" | "Personal"; Mode: "Guild";
RecipeType: string; RecipeType: string;
TechProductCategory?: string;
CategoryItemId?: string;
} }
interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> { interface IGuildTechBuyRequest {
Action: string; Action: string;
Mode: "Guild";
RecipeType: string;
} }
interface IGuildTechContributeRequest { interface IGuildTechContributeRequest {
Action: "Contribute"; Action: "Contribute";
ResearchId: string; ResearchId: "";
RecipeType: string; RecipeType: string;
RegularCredits: number; RegularCredits: number;
MiscItems: IMiscItem[]; MiscItems: IMiscItem[];
VaultCredits: number; VaultCredits: number;
VaultMiscItems: IMiscItem[]; VaultMiscItems: IMiscItem[];
} }
const getSalvageCategory = (
category: "CrewShipWeapons" | "CrewShipWeaponSkins"
): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => {
return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
};
const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => {
// delete personal tech project
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins";
return finishComponentRepair(inventory, category, itemId);
};
const finishComponentRepair = (
inventory: TInventoryDatabaseDocument,
category: "CrewShipWeapons" | "CrewShipWeaponSkins",
itemId: string
): IInventoryChanges => {
const salvageCategory = getSalvageCategory(category);
// find salved part & delete it
const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId));
const salvageItem = inventory[salvageCategory][salvageIndex];
inventory[salvageCategory].splice(salvageIndex, 1);
// add final item
const inventoryChanges = {
...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
UpgradeFingerprint: salvageItem.UpgradeFingerprint
})),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
};
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: itemId }
}
];
return inventoryChanges;
};

View File

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

View File

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

View File

@ -1,32 +1,21 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Inbox } from "@/src/models/inboxModel"; import { Inbox } from "@/src/models/inboxModel";
import { import {
createMessage,
createNewEventMessages, createNewEventMessages,
deleteAllMessagesRead, deleteAllMessagesRead,
deleteMessageRead, deleteMessageRead,
getAllMessagesSorted, getAllMessagesSorted,
getMessage getMessage
} from "@/src/services/inboxService"; } from "@/src/services/inboxService";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import { addItems, getInventory } from "@/src/services/inventoryService";
addItems,
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus"; import { ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
export const inboxController: RequestHandler = async (req, res) => { export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const accountId = account._id.toString();
if (deleteId) { if (deleteId) {
if (deleteId === "DeleteAllRead") { if (deleteId === "DeleteAllRead") {
@ -35,17 +24,17 @@ export const inboxController: RequestHandler = async (req, res) => {
return; return;
} }
await deleteMessageRead(parseOid(deleteId as string)); await deleteMessageRead(deleteId as string);
res.status(200).end(); res.status(200).end();
} else if (messageId) { } else if (messageId) {
const message = await getMessage(parseOid(messageId as string)); const message = await getMessage(messageId as string);
message.r = true; message.r = true;
const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt;
if (!attachmentItems && !attachmentCountedItems) {
await message.save(); await message.save();
const attachmentItems = message.attVisualOnly ? undefined : message.att;
const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
res.status(200).end(); res.status(200).end();
return; return;
} }
@ -56,8 +45,8 @@ export const inboxController: RequestHandler = async (req, res) => {
await addItems( await addItems(
inventory, inventory,
attachmentItems.map(attItem => ({ attachmentItems.map(attItem => ({
ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem, ItemType: attItem,
ItemCount: 1 ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})), })),
inventoryChanges inventoryChanges
); );
@ -65,52 +54,15 @@ export const inboxController: RequestHandler = async (req, res) => {
if (attachmentCountedItems) { if (attachmentCountedItems) {
await addItems(inventory, attachmentCountedItems, inventoryChanges); await addItems(inventory, attachmentCountedItems, inventoryChanges);
} }
if (message.gifts) {
const sender = await getAccountFromSuffixedName(message.sndr);
const recipientName = getSuffixedName(account);
const giftQuantity = message.arg!.find(x => x.Key == "GIFT_QUANTITY")!.Tag as number;
for (const gift of message.gifts) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges
);
if (sender) {
await createMessage(sender._id, [
{
sndr: recipientName,
msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody",
arg: [
{
Key: "RECIPIENT_NAME",
Tag: recipientName
},
{
Key: "GIFT_TYPE",
Tag: gift.GiftType
},
{
Key: "GIFT_QUANTITY",
Tag: giftQuantity
}
],
sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
icon: ExportFlavour[getEffectiveAvatarImageType(inventory)].icon,
highPriority: true
}
]);
}
}
}
if (message.RegularCredits) {
updateCurrency(inventory, -message.RegularCredits, false, inventoryChanges);
}
await inventory.save(); await inventory.save();
await message.save();
res.json({ InventoryChanges: inventoryChanges }); res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) { } else if (latestClientMessageId) {
await createNewEventMessages(req); await createNewEventMessages(req);
const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 }); const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 });
const latestClientMessage = messages.find(m => m._id.toString() === parseOid(latestClientMessageId as string)); const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId);
if (!latestClientMessage) { if (!latestClientMessage) {
logger.debug(`this should only happen after DeleteAllRead `); logger.debug(`this should only happen after DeleteAllRead `);
@ -133,11 +85,3 @@ export const inboxController: RequestHandler = async (req, res) => {
res.json({ Inbox: inbox }); res.json({ Inbox: inbox });
} }
}; };
// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb
const parseOid = (oid: string): string => {
if (oid[0] == "{") {
return (JSON.parse(oid) as IOid).$oid;
}
return oid;
};

View File

@ -1,37 +1,36 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { import {
IConsumedSuit, IConsumedSuit,
IHelminthFoodRecord, IHelminthFoodRecord,
IInfestedFoundryClient,
IInfestedFoundryDatabase,
IInventoryClient, IInventoryClient,
IMiscItem, IMiscItem,
InventorySlot InventorySlot,
ITypeCount
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc } from "warframe-public-export-plus"; import { ExportMisc, ExportRecipes } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper"; import { colorToShard } from "@/src/helpers/shardHelper";
import { import { config } from "@/src/services/configService";
addInfestedFoundryXP,
applyCheatsToInfestedFoundry,
handleSubsumeCompletion
} from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
switch (req.query.mode) { switch (req.query.mode) {
case "s": { case "s": {
// shard installation // shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
suit.ArchonCrystalUpgrades ??= []; if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
while (suit.ArchonCrystalUpgrades.length < request.Slot) { suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
suit.ArchonCrystalUpgrades.push({});
} }
suit.ArchonCrystalUpgrades[request.Slot] = { suit.ArchonCrystalUpgrades[request.Slot] = {
UpgradeType: request.UpgradeType, UpgradeType: request.UpgradeType,
@ -56,8 +55,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "x": { case "x": {
// shard removal // shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body)); const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
if (suit.ArchonCrystalUpgrades![request.Slot].Color) { if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
@ -70,35 +69,23 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
ItemCount: 1 ItemCount: 1
}); });
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
// consume resources
if (!inventory.infiniteHelminthMaterials) {
let type: string;
let count: number;
if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
// < 38.6.0
type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile";
count = 300;
} else {
// >= 38.6.0
type =
archonCrystalRemovalResource[
suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "")
];
count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150;
}
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count;
}
} }
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined; suit.ArchonCrystalUpgrades![request.Slot] = {};
suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
if (!config.infiniteHelminthMaterials) {
// remove bile
const bile = inventory.InfestedFoundry!.Resources!.find(
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
)!;
bile.Count -= 300;
}
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(inventory, infestedFoundry); applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
MiscItems: miscItemChanges, MiscItems: miscItemChanges,
@ -111,7 +98,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "n": { case "n": {
// name the beast // name the beast
const request = getJSONfromString<IHelminthNameRequest>(String(req.body)); const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Name = request.newName; inventory.InfestedFoundry.Name = request.newName;
await inventory.save(); await inventory.save();
@ -128,14 +115,13 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "c": { case "c": {
// consume items // consume items
const inventory = await getInventory(account._id.toString()); if (config.infiniteHelminthMaterials) {
if (inventory.infiniteHelminthMaterials) {
res.status(400).end(); res.status(400).end();
return; return;
} }
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body)); const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Resources ??= []; inventory.InfestedFoundry.Resources ??= [];
@ -231,7 +217,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "o": { case "o": {
// offerings update // offerings update
const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body)); const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex; inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes; inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
@ -240,7 +226,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(inventory, infestedFoundry); applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
InfestedFoundry: infestedFoundry InfestedFoundry: infestedFoundry
@ -252,9 +238,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "a": { case "a": {
// subsume warframe // subsume warframe
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body)); const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const recipe = getRecipe(request.Recipe)!; const recipe = getRecipe(request.Recipe)!;
if (!inventory.infiniteHelminthMaterials) { if (!config.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) { for (const ingredient of recipe.secretIngredients!) {
const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType); const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType);
if (resource) { if (resource) {
@ -280,7 +266,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SUITS); freeUpSlot(inventory, InventorySlot.SUITS);
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(inventory, infestedFoundry); applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
Recipes: recipeChanges, Recipes: recipeChanges,
@ -302,12 +288,12 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "r": { case "r": {
// rush subsume // rush subsume
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 50, true); const currencyChanges = updateCurrency(inventory, 50, true);
const recipeChanges = handleSubsumeCompletion(inventory); const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(inventory, infestedFoundry); applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
...currencyChanges, ...currencyChanges,
@ -320,7 +306,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "u": { case "u": {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body)); const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.OffensiveUpgrade = request.OffensiveUpgradeType;
@ -328,7 +314,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
suit.UpgradesExpiry = upgradesExpiry; suit.UpgradesExpiry = upgradesExpiry;
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00);
addRecipes(inventory, recipeChanges); addRecipes(inventory, recipeChanges);
if (!inventory.infiniteHelminthMaterials) { if (!config.infiniteHelminthMaterials) {
for (let i = 0; i != request.ResourceTypes.length; ++i) { for (let i = 0; i != request.ResourceTypes.length; ++i) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -= inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -=
request.ResourceCosts[i]; request.ResourceCosts[i];
@ -338,7 +324,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
inventory.InfestedFoundry!.InvigorationsApplied += 1; inventory.InfestedFoundry!.InvigorationsApplied += 1;
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(inventory, infestedFoundry); applyCheatsToInfestedFoundry(infestedFoundry);
res.json({ res.json({
SuitId: request.SuitId, SuitId: request.SuitId,
OffensiveUpgrade: request.OffensiveUpgradeType, OffensiveUpgrade: request.OffensiveUpgradeType,
@ -353,7 +339,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
case "custom_unlockall": { case "custom_unlockall": {
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.XP ??= 0; inventory.InfestedFoundry.XP ??= 0;
if (151875_00 > inventory.InfestedFoundry.XP) { if (151875_00 > inventory.InfestedFoundry.XP) {
@ -397,11 +383,116 @@ interface IHelminthFeedRequest {
}[]; }[];
} }
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
const recipeChanges: ITypeCount[] = [];
infestedFoundry.XP ??= 0;
const prevXP = infestedFoundry.XP;
infestedFoundry.XP += delta;
if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 3;
}
if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
ItemCount: 1
});
}
if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
}
if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 10;
}
if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
ItemCount: 1
});
}
if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
}
if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 20;
}
if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
}
if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
infestedFoundry.Slots = 1;
}
if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
ItemCount: 1
});
}
if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
ItemCount: 1
});
}
if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
ItemCount: 1
});
}
if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
ItemCount: 1
});
}
if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
ItemCount: 1
});
}
return recipeChanges;
};
interface IHelminthSubsumeRequest { interface IHelminthSubsumeRequest {
SuitId: IOid; SuitId: IOid;
Recipe: string; Recipe: string;
} }
export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => {
const [recipeType] = Object.entries(ExportRecipes).find(
([_recipeType, recipe]) =>
recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" &&
recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType
)!;
inventory.InfestedFoundry!.LastConsumedSuit = undefined;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined;
const recipeChanges: ITypeCount[] = [
{
ItemType: recipeType,
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
return recipeChanges;
};
export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => {
if (config.infiniteHelminthMaterials) {
infestedFoundry.Resources = [
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 }
];
}
};
interface IHelminthOfferingsUpdate { interface IHelminthOfferingsUpdate {
OfferingsIndex: number; OfferingsIndex: number;
SuitTypes: string[]; SuitTypes: string[];
@ -452,12 +543,3 @@ const apetiteModel = (x: number): number => {
} }
return 3; return 3;
}; };
const archonCrystalRemovalResource: Record<string, string> = {
ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides",
ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile",
ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics",
ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics",
ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones",
ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx"
};

View File

@ -1,50 +1,26 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json"; import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { import {
eFaction,
ExportCustoms, ExportCustoms,
ExportFlavour, ExportFlavour,
ExportRegions,
ExportResources, ExportResources,
ExportVirtuals, ExportVirtuals
ICountedItem
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController";
import { import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
addEmailItem,
addItem,
addMiscItems,
allDailyAffiliationKeys,
checkCalendarAutoAdvance,
cleanupInventory,
createLibraryDailyTask,
getCalendarProgress
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { addString, catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
import { Inbox } from "@/src/models/inboxModel";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { DailyDeal } from "@/src/models/worldStateModel";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
import { generateRewardSeed } from "@/src/services/rngService";
import { getInvasionByOid, getWorldState } from "@/src/services/worldStateService";
import { createMessage } from "@/src/services/inboxService";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const accountId = await getAccountIdForRequest(request);
const inventory = await Inventory.findOne({ accountOwnerId: account._id }); const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (!inventory) { if (!inventory) {
response.status(400).json({ error: "inventory was undefined" }); response.status(400).json({ error: "inventory was undefined" });
@ -53,24 +29,20 @@ export const inventoryController: RequestHandler = async (request, response) =>
// Handle daily reset // Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
const today = Math.trunc(Date.now() / 86400000);
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500; inventory[key] = 16000 + inventory.PlayerLevel * 500;
} }
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.TradesRemaining = inventory.PlayerLevel;
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
if (inventory.NextRefill) { if (inventory.NextRefill) {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const daysPassed = today - lastLoginDay;
if (config.noArgonCrystalDecay) { if (config.noArgonCrystalDecay) {
inventory.FoundToday = undefined; inventory.FoundToday = undefined;
} else { } else {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const today = Math.trunc(Date.now() / 86400000);
const daysPassed = today - lastLoginDay;
for (let i = 0; i != daysPassed; ++i) { for (let i = 0; i != daysPassed; ++i) {
const numArgonCrystals = const numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
@ -78,11 +50,9 @@ export const inventoryController: RequestHandler = async (request, response) =>
if (numArgonCrystals == 0) { if (numArgonCrystals == 0) {
break; break;
} }
const numStableArgonCrystals = Math.min( const numStableArgonCrystals =
numArgonCrystals,
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0 ?.ItemCount ?? 0;
);
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals; const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2); const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, { logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
@ -102,88 +72,10 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory.FoundToday = undefined; inventory.FoundToday = undefined;
} }
} }
if (inventory.UsedDailyDeals.length != 0) {
if (daysPassed == 1) {
const todayAt0Utc = today * 86400000;
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
const darvoOid =
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
const deal = await DailyDeal.findById(darvoOid);
if (deal) {
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
} else {
inventory.UsedDailyDeals = [];
}
} else {
inventory.UsedDailyDeals = [];
}
}
} }
// TODO: Setup CalendarProgress as part of 1999 mission completion? inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
await inventory.save();
const previousYearIteration = inventory.CalendarProgress?.Iteration;
// We need to do the following to ensure the in-game calendar does not break:
getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364)
checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424)
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress!.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail);
kalymos = false;
break;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem");
}
}
cleanupInventory(inventory);
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
//await inventory.save();
} }
if ( if (
@ -192,120 +84,35 @@ export const inventoryController: RequestHandler = async (request, response) =>
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) { ) {
handleSubsumeCompletion(inventory); handleSubsumeCompletion(inventory);
//await inventory.save();
}
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
}
if (factionSidedWith) {
if (battlePay) {
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
}
]);
}
if (invasion.Faction != "FC_INFESTATION") {
// Sided with grineer -> opposed corpus -> send zanuka (harvester)
// Sided with corpus -> opposed grineer -> send g3 (death squad)
inventory[factionSidedWith != "FC_GRINEER" ? "DeathSquadable" : "Harvestable"] = true;
// TOVERIFY: Should this happen earlier?
// TOVERIFY: Should this send an (ephemeral) email?
}
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
if (!inventory.DuviriInfo) {
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
} else {
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save(); await inventory.save();
}
response.json( response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query, account.BuildLabel)
);
}; };
export const getInventoryResponse = async ( export const getInventoryResponse = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
xpBasedLevelCapDisabled: boolean, xpBasedLevelCapDisabled: boolean
buildLabel: string | undefined
): Promise<IInventoryClient> => { ): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([ const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>(
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), "LoadOutPresets"
Ship.find({ ShipOwnerId: inventory.accountOwnerId }), );
Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 }) const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>(
]); "Ships"
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>(); );
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>()); const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON<IInventoryClient>();
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it. if (config.infiniteCredits) {
if (latestMessage) {
inventoryResponse.Mailbox = {
LastInboxId: toOid(latestMessage._id)
};
}
if (inventory.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
} }
if (inventory.infinitePlatinum) { if (config.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 0; inventoryResponse.PremiumCreditsFree = 0;
inventoryResponse.PremiumCredits = 999999999; inventoryResponse.PremiumCredits = 999999999;
} }
if (inventory.infiniteEndo) { if (config.infiniteEndo) {
inventoryResponse.FusionPoints = 999999999; inventoryResponse.FusionPoints = 999999999;
} }
if (inventory.infiniteRegalAya) { if (config.infiniteRegalAya) {
inventoryResponse.PrimeTokens = 999999999; inventoryResponse.PrimeTokens = 999999999;
} }
@ -321,11 +128,23 @@ export const getInventoryResponse = async (
} }
} }
if (config.unlockAllMissions) {
inventoryResponse.Missions = [];
for (const tag of Object.keys(ExportRegions)) {
inventoryResponse.Missions.push({
Completes: 1,
Tier: 1,
Tag: tag
});
}
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
}
if (config.unlockAllShipDecorations) { if (config.unlockAllShipDecorations) {
inventoryResponse.ShipDecorations = []; inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) { for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") { if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 }); inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
} }
} }
} }
@ -335,17 +154,6 @@ export const getInventoryResponse = async (
for (const uniqueName in ExportFlavour) { for (const uniqueName in ExportFlavour) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName }); inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
} }
} else if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess"
].forEach(uniqueName => {
if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
});
} }
if (config.unlockAllSkins) { if (config.unlockAllSkins) {
@ -389,8 +197,7 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) { if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = []; const Polarity: IPolarity[] = [];
// 12 is needed for necramechs. 15 is needed for plexus/crewshipharness. for (let i = 0; i != 12; ++i) {
for (let i = 0; i != 15; ++i) {
Polarity.push({ Polarity.push({
Slot: i, Slot: i,
Value: ArtifactPolarity.Any Value: ArtifactPolarity.Any
@ -445,85 +252,24 @@ export const getInventoryResponse = async (
} }
} }
if (config.noDailyFocusLimit) { if (inventoryResponse.InfestedFoundry) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000); applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
} }
if (inventoryResponse.InfestedFoundry) { // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
applyCheatsToInfestedFoundry(inventory, inventoryResponse.InfestedFoundry); //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
}
// Set 2FA enabled so trading post can be used // Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true; inventoryResponse.HWIDProtectEnabled = true;
if (buildLabel) {
// Fix nemesis for older versions
if (
inventoryResponse.Nemesis &&
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
) {
inventoryResponse.Nemesis = undefined;
}
if (version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
// U19.5 and below use $id instead of $oid
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
}
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
}
}
}
if (config.unlockAllProfitTakerStages) {
inventoryResponse.CompletedJobChains ??= [];
const EudicoHeists = inventoryResponse.CompletedJobChains.find(x => x.LocationTag == "EudicoHeists");
if (EudicoHeists) {
EudicoHeists.Jobs = allEudicoHeistJobs;
} else {
inventoryResponse.CompletedJobChains.push({
LocationTag: "EudicoHeists",
Jobs: allEudicoHeistJobs
});
}
}
if (config.unlockAllSimarisResearchEntries) {
inventoryResponse.LibraryPersonalTarget = undefined;
inventoryResponse.LibraryPersonalProgress = [
"/Lotus/Types/Game/Library/Targets/Research1Target",
"/Lotus/Types/Game/Library/Targets/Research2Target",
"/Lotus/Types/Game/Library/Targets/Research3Target",
"/Lotus/Types/Game/Library/Targets/Research4Target",
"/Lotus/Types/Game/Library/Targets/Research5Target",
"/Lotus/Types/Game/Library/Targets/Research6Target",
"/Lotus/Types/Game/Library/Targets/Research7Target"
].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
}
return inventoryResponse; return inventoryResponse;
}; };
const allEudicoHeistJobs = [ export const addString = (arr: string[], str: string): void => {
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne", if (!arr.find(x => x == str)) {
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyTwo", arr.push(str);
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree", }
"/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyFour" };
];
const getExpRequiredForMr = (rank: number): number => { const getExpRequiredForMr = (rank: number): number => {
if (rank <= 30) { if (rank <= 30) {
@ -549,3 +295,13 @@ const resourceGetParent = (resourceName: string): string | undefined => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName; return ExportVirtuals[resourceName]?.parentName;
}; };
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
export const catBreadHash = (name: string): number => {
let hash = 2166136261;
for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
hash = (hash * 16777619) & 0x7fffffff;
}
return hash;
};

View File

@ -1,8 +1,9 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency, updateSlots } from "@/src/services/inventoryService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { updateSlots } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { exhaustive } from "@/src/utils/ts-utils"; import { logger } from "@/src/utils/logger";
/* /*
loadout slots are additionally purchased slots only loadout slots are additionally purchased slots only
@ -22,44 +23,13 @@ export const inventorySlotsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const body = JSON.parse(req.body as string) as IInventorySlotsRequest; const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
let price; if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) {
let amount; logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`);
switch (body.Bin) {
case InventorySlot.SUITS:
case InventorySlot.MECHSUITS:
case InventorySlot.PVE_LOADOUTS:
case InventorySlot.CREWMEMBERS:
price = 20;
amount = 1;
break;
case InventorySlot.SPACESUITS:
price = 12;
amount = 1;
break;
case InventorySlot.WEAPONS:
case InventorySlot.SPACEWEAPONS:
case InventorySlot.SENTINELS:
case InventorySlot.RJ_COMPONENT_AND_ARMAMENTS:
case InventorySlot.AMPS:
price = 12;
amount = 2;
break;
case InventorySlot.RIVENS:
price = 60;
amount = 3;
break;
default:
exhaustive(body.Bin);
throw new Error(`unexpected slot purchase of type ${body.Bin as string}`);
} }
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, price, true); const currencyChanges = updateCurrency(inventory, 20, true);
updateSlots(inventory, body.Bin, amount, amount); updateSlots(inventory, body.Bin, 1, 1);
await inventory.save(); await inventory.save();
res.json({ InventoryChanges: currencyChanges }); res.json({ InventoryChanges: currencyChanges });

View File

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

View File

@ -1,45 +1,31 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse } from "@/src/services/loginRewardService";
claimLoginReward,
getRandomLoginRewards,
ILoginRewardsReponse,
isLoginRewardAChoice,
setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const loginRewardsController: RequestHandler = async (req, res) => { export const loginRewardsController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
const today = Math.trunc(Date.now() / 86400000) * 86400; const today = Math.trunc(Date.now() / 86400000) * 86400;
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50;
if (today == account.LastLoginRewardDate || config.disableDailyTribute) { if (today == account.LastLoginRewardDate) {
res.json({ res.end();
DailyTributeInfo: {
IsMilestoneDay: isMilestoneDay,
IsChooseRewardSet: isLoginRewardAChoice(account),
LoginDays: account.LoginDays,
NextMilestoneReward: "",
NextMilestoneDay: nextMilestoneDay
}
} satisfies ILoginRewardsReponse);
return; return;
} }
account.LoginDays += 1;
account.LastLoginRewardDate = today;
await account.save();
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(account._id.toString());
const randomRewards = getRandomLoginRewards(account, inventory); const randomRewards = getRandomLoginRewards(account, inventory);
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const response: ILoginRewardsReponse = { const response: ILoginRewardsReponse = {
DailyTributeInfo: { DailyTributeInfo: {
Rewards: randomRewards, Rewards: randomRewards,
IsMilestoneDay: isMilestoneDay, IsMilestoneDay: isMilestoneDay,
IsChooseRewardSet: randomRewards.length != 1, IsChooseRewardSet: randomRewards.length != 1,
LoginDays: account.LoginDays, LoginDays: account.LoginDays,
NextMilestoneReward: "", //NextMilestoneReward: "",
NextMilestoneDay: nextMilestoneDay, NextMilestoneDay: account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50,
HasChosenReward: false HasChosenReward: false
}, },
LastLoginRewardDate: today LastLoginRewardDate: today
@ -48,10 +34,7 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
response.DailyTributeInfo.HasChosenReward = true; response.DailyTributeInfo.HasChosenReward = true;
response.DailyTributeInfo.ChosenReward = randomRewards[0]; response.DailyTributeInfo.ChosenReward = randomRewards[0];
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
setAccountGotLoginRewardToday(account); await inventory.save();
await Promise.all([inventory.save(), account.save()]);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
} }
res.json(response); res.json(response);
}; };

View File

@ -1,12 +1,7 @@
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService";
claimLoginReward,
getRandomLoginRewards,
setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -27,7 +22,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
StoreItemType: body.ChosenReward StoreItemType: body.ChosenReward
}; };
inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges; inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
if (evergreenRewards.indexOf(body.ChosenReward) == -1) { if (!evergreenRewards.find(x => x == body.ChosenReward)) {
inventory.LoginMilestoneRewards.push(body.ChosenReward); inventory.LoginMilestoneRewards.push(body.ChosenReward);
} }
} else { } else {
@ -35,10 +30,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
inventoryChanges = await claimLoginReward(inventory, chosenReward); inventoryChanges = await claimLoginReward(inventory, chosenReward);
} }
setAccountGotLoginRewardToday(account); await inventory.save();
await Promise.all([inventory.save(), account.save()]);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
res.json({ res.json({
DailyTributeInfo: { DailyTributeInfo: {
NewInventory: inventoryChanges, NewInventory: inventoryChanges,

View File

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

View File

@ -1,27 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const maturePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets");
const data = getJSONfromString<IMaturePetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.IsPuppy = data.revert;
await inventory.save();
res.json({
petId: data.petId,
updateCollar: true,
armorSkins: ["", "", ""],
furPatterns: data.revert
? ["", "", ""]
: [details.DominantTraits.FurPattern, details.DominantTraits.FurPattern, details.DominantTraits.FurPattern],
unmature: data.revert
});
};
interface IMaturePetRequest {
petId: string;
revert: boolean;
}

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