Compare commits
	
		
			1 Commits
		
	
	
		
			main
			...
			syndicate-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 8378797c36 | 
@ -3,6 +3,3 @@
 | 
			
		||||
Dockerfile*
 | 
			
		||||
.*
 | 
			
		||||
docker-data/
 | 
			
		||||
node_modules/
 | 
			
		||||
static/data/
 | 
			
		||||
logs/
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										38
									
								
								.eslintrc
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								.eslintrc
									
									
									
									
									
								
							@ -1,48 +1,36 @@
 | 
			
		||||
{
 | 
			
		||||
    "plugins": ["@typescript-eslint", "prettier", "import"],
 | 
			
		||||
    "extends": [
 | 
			
		||||
        "eslint:recommended",
 | 
			
		||||
        "plugin:@typescript-eslint/recommended",
 | 
			
		||||
        "plugin:@typescript-eslint/recommended-requiring-type-checking",
 | 
			
		||||
        "plugin:import/recommended",
 | 
			
		||||
        "plugin:import/typescript"
 | 
			
		||||
        "plugin:@typescript-eslint/recommended-requiring-type-checking"
 | 
			
		||||
    ],
 | 
			
		||||
    "plugins": ["@typescript-eslint", "prettier"],
 | 
			
		||||
    "env": {
 | 
			
		||||
        "browser": true,
 | 
			
		||||
        "es6": true,
 | 
			
		||||
        "node": true
 | 
			
		||||
    },
 | 
			
		||||
    "rules": {
 | 
			
		||||
        "@typescript-eslint/consistent-type-imports": "error",
 | 
			
		||||
        "@typescript-eslint/explicit-function-return-type": "error",
 | 
			
		||||
        "@typescript-eslint/restrict-template-expressions": "error",
 | 
			
		||||
        "@typescript-eslint/restrict-plus-operands": "error",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-member-access": "error",
 | 
			
		||||
        "@typescript-eslint/explicit-function-return-type": "warn",
 | 
			
		||||
        "@typescript-eslint/restrict-template-expressions": "warn",
 | 
			
		||||
        "@typescript-eslint/restrict-plus-operands": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-member-access": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
 | 
			
		||||
        "@typescript-eslint/no-unsafe-argument": "error",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-call": "error",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-assignment": "error",
 | 
			
		||||
        "@typescript-eslint/no-explicit-any": "off",
 | 
			
		||||
        "no-loss-of-precision": "error",
 | 
			
		||||
        "@typescript-eslint/no-unnecessary-condition": "error",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-call": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unsafe-assignment": "warn",
 | 
			
		||||
        "@typescript-eslint/no-explicit-any": "warn",
 | 
			
		||||
        "no-loss-of-precision": "warn",
 | 
			
		||||
        "@typescript-eslint/no-unnecessary-condition": "warn",
 | 
			
		||||
        "@typescript-eslint/no-base-to-string": "off",
 | 
			
		||||
        "no-case-declarations": "error",
 | 
			
		||||
        "prettier/prettier": "error",
 | 
			
		||||
        "no-mixed-spaces-and-tabs": "error",
 | 
			
		||||
        "@typescript-eslint/require-await": "error",
 | 
			
		||||
        "import/no-named-as-default-member": "off",
 | 
			
		||||
        "import/no-cycle": "warn",
 | 
			
		||||
        "@typescript-eslint/no-deprecated": "warn"
 | 
			
		||||
        "require-await": "off",
 | 
			
		||||
        "@typescript-eslint/require-await": "error"
 | 
			
		||||
    },
 | 
			
		||||
    "parser": "@typescript-eslint/parser",
 | 
			
		||||
    "parserOptions": {
 | 
			
		||||
        "project": "./tsconfig.json"
 | 
			
		||||
    },
 | 
			
		||||
    "settings": {
 | 
			
		||||
        "import/extensions": [ ".ts" ],
 | 
			
		||||
        "import/resolver": {
 | 
			
		||||
            "typescript": true,
 | 
			
		||||
            "node": true
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
# Auto detect text files and perform LF normalization
 | 
			
		||||
* text=auto eol=lf
 | 
			
		||||
* text=auto
 | 
			
		||||
 | 
			
		||||
static/webui/libs/ linguist-vendored
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@ -1,7 +1,6 @@
 | 
			
		||||
name: Build
 | 
			
		||||
on:
 | 
			
		||||
    push:
 | 
			
		||||
        branches: ["main"]
 | 
			
		||||
    push: {}
 | 
			
		||||
    pull_request: {}
 | 
			
		||||
jobs:
 | 
			
		||||
    build:
 | 
			
		||||
@ -11,19 +10,16 @@ jobs:
 | 
			
		||||
              uses: actions/checkout@v4.1.2
 | 
			
		||||
            - name: Setup Node.js environment
 | 
			
		||||
              uses: actions/setup-node@v4.0.2
 | 
			
		||||
              with:
 | 
			
		||||
                  node-version: ">=20.18.1"
 | 
			
		||||
            - run: npm ci
 | 
			
		||||
            - run: cp config-vanilla.json config.json
 | 
			
		||||
            - run: cp config.json.example config.json
 | 
			
		||||
            - run: npm run verify
 | 
			
		||||
            - run: npm run lint:ci
 | 
			
		||||
            - run: npm run prettier
 | 
			
		||||
            - run: npm run update-translations
 | 
			
		||||
            - name: Fail if there are uncommitted changes
 | 
			
		||||
              run: |
 | 
			
		||||
                  if [[ -n "$(git status --porcelain)" ]]; then
 | 
			
		||||
                    echo "Uncommitted changes detected:"
 | 
			
		||||
                    git status
 | 
			
		||||
                    git --no-pager diff
 | 
			
		||||
                    git diff
 | 
			
		||||
                    exit 1
 | 
			
		||||
                  fi
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							@ -18,10 +18,8 @@ jobs:
 | 
			
		||||
            - name: Build and push
 | 
			
		||||
              uses: docker/build-push-action@v6
 | 
			
		||||
              with:
 | 
			
		||||
                  platforms: linux/arm64,linux/amd64
 | 
			
		||||
                  platforms: linux/amd64,linux/arm64
 | 
			
		||||
                  push: true
 | 
			
		||||
                  tags: |
 | 
			
		||||
                      openwf/spaceninjaserver:latest
 | 
			
		||||
                      openwf/spaceninjaserver:latest-arm64
 | 
			
		||||
                      openwf/spaceninjaserver:${{ github.sha }}
 | 
			
		||||
                      openwf/spaceninjaserver:${{ github.sha }}-arm64
 | 
			
		||||
 | 
			
		||||
@ -2,4 +2,3 @@ src/routes/api.ts
 | 
			
		||||
static/webui/libs/
 | 
			
		||||
*.html
 | 
			
		||||
*.md
 | 
			
		||||
config-vanilla.json
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							@ -8,7 +8,8 @@
 | 
			
		||||
      "type": "node",
 | 
			
		||||
      "request": "launch",
 | 
			
		||||
      "name": "Debug and Watch",
 | 
			
		||||
      "args": ["${workspaceFolder}/scripts/dev.js"],
 | 
			
		||||
      "runtimeArgs": ["-r", "tsconfig-paths/register", "-r", "ts-node/register", "--watch-path", "src"],
 | 
			
		||||
      "args": ["${workspaceFolder}/src/index.ts"],
 | 
			
		||||
      "console": "integratedTerminal"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@ -1,3 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "typescript.preferences.preferTypeOnlyAutoImports": true
 | 
			
		||||
}
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
## In General
 | 
			
		||||
 | 
			
		||||
### Prerequisites
 | 
			
		||||
 | 
			
		||||
Use `npm i` or `npm ci` to install all dependencies, including dev dependencies.
 | 
			
		||||
 | 
			
		||||
## Development Process
 | 
			
		||||
 | 
			
		||||
Auto reloading is supported for server and WebUI development. Simply use `npm run dev` or `npm run dev:bun` to start the server and edit away.
 | 
			
		||||
 | 
			
		||||
### Testing
 | 
			
		||||
 | 
			
		||||
Before submitting a PR:
 | 
			
		||||
- Use `npm run verify` to verify that the code is type-safe.
 | 
			
		||||
- Use `npm run fix` to fix formatting issues as well as be informed of any unfixable issues. Avoid introducing new warnings.
 | 
			
		||||
 | 
			
		||||
## WebUI Specific
 | 
			
		||||
 | 
			
		||||
The translation system is designed around additions being made to `static/webui/translations/en.js`. They are copied over for translation via `npm run update-translations`. DO NOT provide translations generated by AI or other automated tools.
 | 
			
		||||
							
								
								
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,11 +1,26 @@
 | 
			
		||||
FROM node:24-alpine3.21
 | 
			
		||||
FROM node:18-alpine3.19
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache bash jq
 | 
			
		||||
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF
 | 
			
		||||
ENV APP_MY_ADDRESS=localhost
 | 
			
		||||
ENV APP_HTTP_PORT=80
 | 
			
		||||
ENV APP_HTTPS_PORT=443
 | 
			
		||||
ENV APP_AUTO_CREATE_ACCOUNT=true
 | 
			
		||||
ENV APP_SKIP_STORY_MODE_CHOICE=true
 | 
			
		||||
ENV APP_SKIP_TUTORIAL=true
 | 
			
		||||
ENV APP_SKIP_ALL_DIALOGUE=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SCANS=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_MISSIONS=true
 | 
			
		||||
ENV APP_INFINITE_RESOURCES=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=true
 | 
			
		||||
ENV APP_UNLOCK_ALL_SKINS=true
 | 
			
		||||
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=true
 | 
			
		||||
ENV APP_SPOOF_MASTERY_RANK=-1
 | 
			
		||||
 | 
			
		||||
RUN apk add --no-cache bash sed wget jq
 | 
			
		||||
 | 
			
		||||
COPY . /app
 | 
			
		||||
WORKDIR /app
 | 
			
		||||
 | 
			
		||||
RUN npm i --omit=dev --omit=optional
 | 
			
		||||
RUN date '+%d %B %Y' > BUILD_DATE
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										32
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.md
									
									
									
									
									
								
							@ -6,36 +6,10 @@ More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://di
 | 
			
		||||
 | 
			
		||||
This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
 | 
			
		||||
 | 
			
		||||
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
 | 
			
		||||
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
 | 
			
		||||
 | 
			
		||||
## config.json
 | 
			
		||||
 | 
			
		||||
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled.
 | 
			
		||||
 | 
			
		||||
- `skipTutorial` affects only newly created accounts, so you may wish to change it before logging in for the first time.
 | 
			
		||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
 | 
			
		||||
- `ircExecutable` can be provided with a relative path to an EXE which will be ran as a child process of SpaceNinjaServer.
 | 
			
		||||
- `ircAddress`, `hubAddress`, and `nrsAddress` can be provided if these secondary servers are on a different machine.
 | 
			
		||||
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
 | 
			
		||||
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
 | 
			
		||||
- `worldState.duviriOverride` can be set to `joy`, `anger`, `envy`, `sorrow`, or `fear` to lock the Duviri spiral.
 | 
			
		||||
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
 | 
			
		||||
  - `RadioLegionIntermission14Syndicate` for Nora's Mix: Dreams of the Dead
 | 
			
		||||
  - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
 | 
			
		||||
  - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
 | 
			
		||||
  - `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
 | 
			
		||||
  - `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
 | 
			
		||||
  - `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
 | 
			
		||||
  - `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
 | 
			
		||||
  - `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
 | 
			
		||||
  - `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
 | 
			
		||||
  - `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
 | 
			
		||||
  - `RadioLegionIntermission4Syndicate` for Nora's Choice
 | 
			
		||||
  - `RadioLegionIntermission3Syndicate` for Intermission III
 | 
			
		||||
  - `RadioLegion3Syndicate` for Glassmaker
 | 
			
		||||
  - `RadioLegionIntermission2Syndicate` for Intermission II
 | 
			
		||||
  - `RadioLegion2Syndicate` for The Emissary
 | 
			
		||||
  - `RadioLegionIntermissionSyndicate` for Intermission I
 | 
			
		||||
  - `RadioLegionSyndicate` for The Wolf of Saturn Six
 | 
			
		||||
- `worldState.allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
 | 
			
		||||
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.
 | 
			
		||||
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
 | 
			
		||||
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
 | 
			
		||||
 | 
			
		||||
@ -1,33 +1,26 @@
 | 
			
		||||
@echo off
 | 
			
		||||
 | 
			
		||||
echo Updating SpaceNinjaServer...
 | 
			
		||||
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
 | 
			
		||||
git fetch --prune
 | 
			
		||||
if %errorlevel% == 0 (
 | 
			
		||||
	git stash
 | 
			
		||||
	git checkout -f origin/main
 | 
			
		||||
git stash
 | 
			
		||||
git reset --hard origin/main
 | 
			
		||||
 | 
			
		||||
	if exist static\data\0\ (
 | 
			
		||||
if exist static\data\0\ (
 | 
			
		||||
	echo Updating stripped assets...
 | 
			
		||||
	cd static\data\0\
 | 
			
		||||
	git pull
 | 
			
		||||
	cd ..\..\..\
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	echo Updating dependencies...
 | 
			
		||||
	node scripts/raw-precheck.js > NUL
 | 
			
		||||
	if %errorlevel% == 0 (
 | 
			
		||||
		call npm i --omit=dev --omit=optional
 | 
			
		||||
		call npm run raw
 | 
			
		||||
	) else (
 | 
			
		||||
		call npm i --omit=dev
 | 
			
		||||
		call npm run build
 | 
			
		||||
		if %errorlevel% == 0 (
 | 
			
		||||
			call npm run start
 | 
			
		||||
		)
 | 
			
		||||
	)
 | 
			
		||||
	echo SpaceNinjaServer seems to have crashed.
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
echo Updating dependencies...
 | 
			
		||||
call npm i --omit=dev
 | 
			
		||||
 | 
			
		||||
call npm run build
 | 
			
		||||
if %errorlevel% == 0 (
 | 
			
		||||
	call npm run start
 | 
			
		||||
	echo SpaceNinjaServer seems to have crashed.
 | 
			
		||||
)
 | 
			
		||||
:a
 | 
			
		||||
pause > nul
 | 
			
		||||
goto a
 | 
			
		||||
 | 
			
		||||
@ -1,29 +0,0 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
 | 
			
		||||
echo "Updating SpaceNinjaServer..."
 | 
			
		||||
git fetch --prune
 | 
			
		||||
if [ $? -eq 0 ]; then
 | 
			
		||||
    git stash
 | 
			
		||||
    git checkout -f origin/main
 | 
			
		||||
 | 
			
		||||
    if [ -d "static/data/0/" ]; then
 | 
			
		||||
        echo "Updating stripped assets..."
 | 
			
		||||
        cd static/data/0/
 | 
			
		||||
        git pull
 | 
			
		||||
        cd ../../../
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
    echo "Updating dependencies..."
 | 
			
		||||
    node scripts/raw-precheck.js > /dev/null
 | 
			
		||||
    if [ $? -eq 0 ]; then
 | 
			
		||||
        npm i --omit=dev --omit=optional
 | 
			
		||||
        npm run raw
 | 
			
		||||
    else
 | 
			
		||||
        npm i --omit=dev
 | 
			
		||||
        npm run build
 | 
			
		||||
        if [ $? -eq 0 ]; then
 | 
			
		||||
            npm run start
 | 
			
		||||
        fi
 | 
			
		||||
    fi
 | 
			
		||||
    echo "SpaceNinjaServer seems to have crashed."
 | 
			
		||||
fi
 | 
			
		||||
@ -1,74 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
 | 
			
		||||
  "logger": {
 | 
			
		||||
    "files": true,
 | 
			
		||||
    "level": "trace"
 | 
			
		||||
  },
 | 
			
		||||
  "myAddress": "localhost",
 | 
			
		||||
  "bindAddress": "0.0.0.0",
 | 
			
		||||
  "httpPort": 80,
 | 
			
		||||
  "httpsPort": 443,
 | 
			
		||||
  "ircExecutable": null,
 | 
			
		||||
  "ircAddress": null,
 | 
			
		||||
  "hubAddress": null,
 | 
			
		||||
  "nrsAddress": null,
 | 
			
		||||
  "administratorNames": [],
 | 
			
		||||
  "autoCreateAccount": true,
 | 
			
		||||
  "skipTutorial": false,
 | 
			
		||||
  "unlockAllSkins": false,
 | 
			
		||||
  "fullyStockedVendors": false,
 | 
			
		||||
  "skipClanKeyCrafting": false,
 | 
			
		||||
  "unfaithfulBugFixes": {
 | 
			
		||||
    "ignore1999LastRegionPlayed": false,
 | 
			
		||||
    "fixXtraCheeseTimer": false,
 | 
			
		||||
    "useAnniversaryTagForOldGoals": true
 | 
			
		||||
  },
 | 
			
		||||
  "worldState": {
 | 
			
		||||
    "creditBoost": false,
 | 
			
		||||
    "affinityBoost": false,
 | 
			
		||||
    "resourceBoost": false,
 | 
			
		||||
    "tennoLiveRelay": false,
 | 
			
		||||
    "baroTennoConRelay": false,
 | 
			
		||||
    "baroAlwaysAvailable": false,
 | 
			
		||||
    "baroFullyStocked": false,
 | 
			
		||||
    "varziaFullyStocked": false,
 | 
			
		||||
    "wolfHunt": null,
 | 
			
		||||
    "orphixVenom": false,
 | 
			
		||||
    "longShadow": false,
 | 
			
		||||
    "hallowedFlame": false,
 | 
			
		||||
    "anniversary": null,
 | 
			
		||||
    "hallowedNightmares": false,
 | 
			
		||||
    "hallowedNightmaresRewardsOverride": 0,
 | 
			
		||||
    "naberusNightsOverride": null,
 | 
			
		||||
    "proxyRebellion": false,
 | 
			
		||||
    "proxyRebellionRewardsOverride": 0,
 | 
			
		||||
    "voidCorruption2025Week1": false,
 | 
			
		||||
    "voidCorruption2025Week2": false,
 | 
			
		||||
    "voidCorruption2025Week3": false,
 | 
			
		||||
    "voidCorruption2025Week4": false,
 | 
			
		||||
    "qtccAlerts": false,
 | 
			
		||||
    "galleonOfGhouls": 0,
 | 
			
		||||
    "ghoulEmergenceOverride": null,
 | 
			
		||||
    "plagueStarOverride": null,
 | 
			
		||||
    "starDaysOverride": null,
 | 
			
		||||
    "dogDaysOverride": null,
 | 
			
		||||
    "dogDaysRewardsOverride": null,
 | 
			
		||||
    "bellyOfTheBeast": false,
 | 
			
		||||
    "bellyOfTheBeastProgressOverride": 0,
 | 
			
		||||
    "eightClaw": false,
 | 
			
		||||
    "eightClawProgressOverride": 0,
 | 
			
		||||
    "thermiaFracturesOverride": null,
 | 
			
		||||
    "thermiaFracturesProgressOverride": 0,
 | 
			
		||||
    "eidolonOverride": "",
 | 
			
		||||
    "vallisOverride": "",
 | 
			
		||||
    "duviriOverride": "",
 | 
			
		||||
    "nightwaveOverride": "",
 | 
			
		||||
    "allTheFissures": "",
 | 
			
		||||
    "varziaOverride": "",
 | 
			
		||||
    "circuitGameModes": null,
 | 
			
		||||
    "darvoStockMultiplier": 1
 | 
			
		||||
  },
 | 
			
		||||
  "dev": {
 | 
			
		||||
    "keepVendorsExpired": false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								config.json.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								config.json.example
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
{
 | 
			
		||||
  "mongodbUrl": "mongodb://127.0.0.1:27017/openWF",
 | 
			
		||||
  "logger": {
 | 
			
		||||
    "files": true,
 | 
			
		||||
    "level": "trace"
 | 
			
		||||
  },
 | 
			
		||||
  "myAddress": "localhost",
 | 
			
		||||
  "httpPort": 80,
 | 
			
		||||
  "httpsPort": 443,
 | 
			
		||||
  "NRS": ["localhost"],
 | 
			
		||||
  "administratorNames": [],
 | 
			
		||||
  "autoCreateAccount": true,
 | 
			
		||||
  "skipTutorial": false,
 | 
			
		||||
  "skipAllDialogue": false,
 | 
			
		||||
  "unlockAllScans": false,
 | 
			
		||||
  "unlockAllMissions": false,
 | 
			
		||||
  "infiniteCredits": false,
 | 
			
		||||
  "infinitePlatinum": false,
 | 
			
		||||
  "infiniteEndo": false,
 | 
			
		||||
  "infiniteRegalAya": false,
 | 
			
		||||
  "infiniteHelminthMaterials": false,
 | 
			
		||||
  "unlockAllShipFeatures": false,
 | 
			
		||||
  "unlockAllShipDecorations": false,
 | 
			
		||||
  "unlockAllFlavourItems": false,
 | 
			
		||||
  "unlockAllSkins": false,
 | 
			
		||||
  "unlockAllCapturaScenes": false,
 | 
			
		||||
  "universalPolarityEverywhere": false,
 | 
			
		||||
  "unlockDoubleCapacityPotatoesEverywhere": false,
 | 
			
		||||
  "unlockExilusEverywhere": false,
 | 
			
		||||
  "unlockArcanesEverywhere": false,
 | 
			
		||||
  "noDailyStandingLimits": false,
 | 
			
		||||
  "noArgonCrystalDecay": false,
 | 
			
		||||
  "noMasteryRankUpCooldown": false,
 | 
			
		||||
  "noVendorPurchaseLimits": true,
 | 
			
		||||
  "instantResourceExtractorDrones": false,
 | 
			
		||||
  "noDojoRoomBuildStage": false,
 | 
			
		||||
  "fastDojoRoomDestruction": false,
 | 
			
		||||
  "noDojoResearchCosts": false,
 | 
			
		||||
  "noDojoResearchTime": false,
 | 
			
		||||
  "fastClanAscension": false,
 | 
			
		||||
  "spoofMasteryRank": -1,
 | 
			
		||||
  "worldState": {
 | 
			
		||||
    "creditBoost": false,
 | 
			
		||||
    "affinityBoost": false,
 | 
			
		||||
    "resourceBoost": false,
 | 
			
		||||
    "starDays": true,
 | 
			
		||||
    "lockTime": 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,23 +1,43 @@
 | 
			
		||||
services:
 | 
			
		||||
    spaceninjaserver:
 | 
			
		||||
        # build: .
 | 
			
		||||
        image: openwf/spaceninjaserver:latest
 | 
			
		||||
        environment:
 | 
			
		||||
            APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
 | 
			
		||||
 | 
			
		||||
            # Following environment variables are set to default image values.
 | 
			
		||||
            # Uncomment to edit.
 | 
			
		||||
 | 
			
		||||
            # APP_MY_ADDRESS: localhost
 | 
			
		||||
            # APP_HTTP_PORT: 80
 | 
			
		||||
            # APP_HTTPS_PORT: 443
 | 
			
		||||
            # APP_AUTO_CREATE_ACCOUNT: true
 | 
			
		||||
            # APP_SKIP_STORY_MODE_CHOICE: true
 | 
			
		||||
            # APP_SKIP_TUTORIAL: true
 | 
			
		||||
            # APP_SKIP_ALL_DIALOGUE: true
 | 
			
		||||
            # APP_UNLOCK_ALL_SCANS: true
 | 
			
		||||
            # APP_UNLOCK_ALL_MISSIONS: true
 | 
			
		||||
            # APP_UNLOCK_ALL_QUESTS: true
 | 
			
		||||
            # APP_COMPLETE_ALL_QUESTS: true
 | 
			
		||||
            # APP_INFINITE_RESOURCES: true
 | 
			
		||||
            # APP_UNLOCK_ALL_SHIP_FEATURES: true
 | 
			
		||||
            # APP_UNLOCK_ALL_SHIP_DECORATIONS: true
 | 
			
		||||
            # APP_UNLOCK_ALL_FLAVOUR_ITEMS: true
 | 
			
		||||
            # APP_UNLOCK_ALL_SKINS: true
 | 
			
		||||
            # APP_UNIVERSAL_POLARITY_EVERYWHERE: true
 | 
			
		||||
            # APP_SPOOF_MASTERY_RANK: -1
 | 
			
		||||
        volumes:
 | 
			
		||||
            - ./docker-data/conf:/app/conf
 | 
			
		||||
            - ./docker-data/static-data:/app/static/data
 | 
			
		||||
            - ./docker-data/static:/app/static/data
 | 
			
		||||
            - ./docker-data/logs:/app/logs
 | 
			
		||||
        ports:
 | 
			
		||||
            - 80:80
 | 
			
		||||
            - 443:443
 | 
			
		||||
 | 
			
		||||
        # Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
 | 
			
		||||
        #build: .
 | 
			
		||||
        # Works best when using `docker-compose up --force-recreate --build`.
 | 
			
		||||
 | 
			
		||||
        depends_on:
 | 
			
		||||
            - mongodb
 | 
			
		||||
    mongodb:
 | 
			
		||||
        image: docker.io/library/mongo:8.0.0-noble
 | 
			
		||||
        environment:
 | 
			
		||||
            MONGO_INITDB_ROOT_USERNAME: openwfagent
 | 
			
		||||
            MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
 | 
			
		||||
        volumes:
 | 
			
		||||
            - ./docker-data/database:/data/db
 | 
			
		||||
        command: mongod --quiet --logpath /dev/null
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,23 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
if [ ! -f conf/config.json ]; then
 | 
			
		||||
	jq --arg value "mongodb://mongodb:27017/openWF" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
 | 
			
		||||
fi
 | 
			
		||||
# Set up the configuration file using environment variables.
 | 
			
		||||
echo '{
 | 
			
		||||
	"logger": {
 | 
			
		||||
	  "files": true,
 | 
			
		||||
	  "level": "trace",
 | 
			
		||||
	  "__valid_levels": "fatal, error, warn, info, http, debug, trace"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
' > config.json
 | 
			
		||||
 | 
			
		||||
exec npm run raw -- --configPath conf/config.json
 | 
			
		||||
for config in $(env | grep "APP_")
 | 
			
		||||
do
 | 
			
		||||
  var=$(echo "${config}" | tr '[:upper:]' '[:lower:]' | sed 's/app_//g' | sed -E 's/_([a-z])/\U\1/g' | sed 's/=.*//g')
 | 
			
		||||
  val=$(echo "${config}" | sed 's/.*=//g')
 | 
			
		||||
  jq --arg variable "$var" --arg value "$val" '.[$variable] += try [$value|fromjson][] catch $value' config.json > config.tmp
 | 
			
		||||
  mv config.tmp config.json
 | 
			
		||||
done
 | 
			
		||||
 | 
			
		||||
npm install
 | 
			
		||||
exec npm run dev
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										3489
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3489
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										49
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								package.json
									
									
									
									
									
								
							@ -4,63 +4,44 @@
 | 
			
		||||
  "description": "WF Emulator",
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node --enable-source-maps build/src/index.js",
 | 
			
		||||
    "build": "tsgo --inlineSourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:tsc": "tsc --incremental --inlineSourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:dev": "tsgo --inlineSourceMap",
 | 
			
		||||
    "build:dev:tsc": "tsc --incremental --inlineSourceMap",
 | 
			
		||||
    "build-and-start": "npm run build && npm run start",
 | 
			
		||||
    "dev": "node scripts/dev.cjs",
 | 
			
		||||
    "dev:bun": "bun scripts/dev.cjs",
 | 
			
		||||
    "start": "node --import ./build/src/pathman.js build/src/index.js",
 | 
			
		||||
    "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
 | 
			
		||||
    "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "verify": "tsgo --noEmit",
 | 
			
		||||
    "verify:tsc": "tsc --noEmit",
 | 
			
		||||
    "raw": "node scripts/raw-precheck.js && node --experimental-transform-types src/index.ts",
 | 
			
		||||
    "raw:bun": "bun src/index.ts",
 | 
			
		||||
    "lint": "eslint --ext .ts .",
 | 
			
		||||
    "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
 | 
			
		||||
    "lint:fix": "eslint --fix --ext .ts .",
 | 
			
		||||
    "prettier": "prettier --write .",
 | 
			
		||||
    "update-translations": "cd scripts && node update-translations.cjs",
 | 
			
		||||
    "fix": "npm run update-translations && npm run prettier"
 | 
			
		||||
    "update-translations": "cd scripts && node update-translations.js"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "GNU",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "body-parser": "^2.2.0",
 | 
			
		||||
    "chokidar": "^4.0.3",
 | 
			
		||||
    "@types/express": "^5",
 | 
			
		||||
    "@types/morgan": "^1.9.9",
 | 
			
		||||
    "crc-32": "^1.2.2",
 | 
			
		||||
    "express": "^5",
 | 
			
		||||
    "json-with-bigint": "^3.4.4",
 | 
			
		||||
    "json-with-bigint": "^3.2.2",
 | 
			
		||||
    "mongoose": "^8.11.0",
 | 
			
		||||
    "morgan": "^1.10.0",
 | 
			
		||||
    "ncp": "^2.0.0",
 | 
			
		||||
    "undici": "^7.10.0",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.93",
 | 
			
		||||
    "typescript": ">=5.5 <5.6.0",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.52",
 | 
			
		||||
    "warframe-riven-info": "^0.1.2",
 | 
			
		||||
    "winston": "^3.17.0",
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0",
 | 
			
		||||
    "ws": "^8.18.2"
 | 
			
		||||
  },
 | 
			
		||||
  "optionalDependencies": {
 | 
			
		||||
    "@types/body-parser": "^1.19.6",
 | 
			
		||||
    "@types/express": "^5",
 | 
			
		||||
    "@types/morgan": "^1.9.9",
 | 
			
		||||
    "@types/websocket": "^1.0.10",
 | 
			
		||||
    "@types/ws": "^8.18.1",
 | 
			
		||||
    "@typescript/native-preview": "^7.0.0-dev.20250625.1",
 | 
			
		||||
    "typescript": "^5.7"
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@rxliuli/tsgo": "^2025.3.31",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^8.28.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^8.28.0",
 | 
			
		||||
    "eslint": "^8",
 | 
			
		||||
    "eslint-import-resolver-typescript": "^4.4.4",
 | 
			
		||||
    "eslint-plugin-import": "^2.32.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.2.5",
 | 
			
		||||
    "prettier": "^3.5.3",
 | 
			
		||||
    "tree-kill": "^1.2.2"
 | 
			
		||||
    "ts-node-dev": "^2.0.0",
 | 
			
		||||
    "tsconfig-paths": "^4.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "node": ">=20.18.1"
 | 
			
		||||
    "node": ">=18.15.0",
 | 
			
		||||
    "npm": ">=9.5.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,81 +0,0 @@
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
const { spawn } = require("child_process");
 | 
			
		||||
const chokidar = require("chokidar");
 | 
			
		||||
const kill = require("tree-kill");
 | 
			
		||||
 | 
			
		||||
let secret = "";
 | 
			
		||||
for (let i = 0; i != 10; ++i) {
 | 
			
		||||
    secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const args = [...process.argv].splice(2);
 | 
			
		||||
args.push("--dev");
 | 
			
		||||
args.push("--secret");
 | 
			
		||||
args.push(secret);
 | 
			
		||||
 | 
			
		||||
const cangoraw = (() => {
 | 
			
		||||
    if (process.versions.bun) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    const [major, minor] = process.versions.node.split(".").map(x => parseInt(x));
 | 
			
		||||
    if (major > 22 || (major == 22 && minor >= 7)) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
})();
 | 
			
		||||
 | 
			
		||||
let buildproc, runproc;
 | 
			
		||||
const spawnopts = { stdio: "inherit", shell: true };
 | 
			
		||||
function run(changedFile) {
 | 
			
		||||
    if (changedFile) {
 | 
			
		||||
        console.log(`Change to ${changedFile} detected`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (buildproc) {
 | 
			
		||||
        kill(buildproc.pid);
 | 
			
		||||
        buildproc = undefined;
 | 
			
		||||
    }
 | 
			
		||||
    if (runproc) {
 | 
			
		||||
        kill(runproc.pid);
 | 
			
		||||
        runproc = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const thisbuildproc = spawn(
 | 
			
		||||
        [process.versions.bun ? "bun" : "npm", "run", cangoraw ? "verify" : "build:dev"].join(" "),
 | 
			
		||||
        spawnopts
 | 
			
		||||
    );
 | 
			
		||||
    const thisbuildstart = Date.now();
 | 
			
		||||
    buildproc = thisbuildproc;
 | 
			
		||||
    buildproc.on("exit", code => {
 | 
			
		||||
        if (buildproc !== thisbuildproc) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        buildproc = undefined;
 | 
			
		||||
        if (code === 0) {
 | 
			
		||||
            console.log(`${cangoraw ? "Verified" : "Built"} in ${Date.now() - thisbuildstart} ms`);
 | 
			
		||||
            runproc = spawn(
 | 
			
		||||
                [
 | 
			
		||||
                    process.versions.bun ? "bun" : "npm",
 | 
			
		||||
                    "run",
 | 
			
		||||
                    cangoraw ? (process.versions.bun ? "raw:bun" : "raw") : "start",
 | 
			
		||||
                    "--",
 | 
			
		||||
                    ...args
 | 
			
		||||
                ].join(" "),
 | 
			
		||||
                spawnopts
 | 
			
		||||
            );
 | 
			
		||||
            runproc.on("exit", () => {
 | 
			
		||||
                runproc = undefined;
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
run();
 | 
			
		||||
chokidar.watch("src").on("change", run);
 | 
			
		||||
chokidar.watch("static/fixed_responses").on("change", run);
 | 
			
		||||
 | 
			
		||||
chokidar.watch("static/webui").on("change", async () => {
 | 
			
		||||
    try {
 | 
			
		||||
        await fetch("http://localhost/custom/webuiFileChangeDetected?secret=" + secret);
 | 
			
		||||
    } catch (e) {}
 | 
			
		||||
});
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
const [major, minor] = process.versions.node.split(".").map(x => parseInt(x));
 | 
			
		||||
if (major > 22 || (major == 22 && minor >= 7)) {
 | 
			
		||||
    // ok
 | 
			
		||||
} else {
 | 
			
		||||
    console.log("Sorry, your Node version is a bit too old for this. You have 2 options:");
 | 
			
		||||
    console.log("- Update Node.js.");
 | 
			
		||||
    console.log("- Use 'npm run build && npm run start'. Optional libraries must be installed for this.");
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +1,10 @@
 | 
			
		||||
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
 | 
			
		||||
// Converted via ChatGPT-4o
 | 
			
		||||
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
 | 
			
		||||
function extractStrings(content) {
 | 
			
		||||
    const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
 | 
			
		||||
    const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
 | 
			
		||||
    let matches;
 | 
			
		||||
    const strings = {};
 | 
			
		||||
    while ((matches = regex.exec(content)) !== null) {
 | 
			
		||||
@ -16,7 +15,7 @@ function extractStrings(content) {
 | 
			
		||||
 | 
			
		||||
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
 | 
			
		||||
const sourceStrings = extractStrings(source);
 | 
			
		||||
const sourceLines = source.substring(0, source.length - 1).split("\n");
 | 
			
		||||
const sourceLines = source.split("\n");
 | 
			
		||||
 | 
			
		||||
fs.readdirSync("../static/webui/translations").forEach(file => {
 | 
			
		||||
    if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
 | 
			
		||||
@ -31,13 +30,13 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
 | 
			
		||||
            const strings = extractStrings(line);
 | 
			
		||||
            if (Object.keys(strings).length > 0) {
 | 
			
		||||
                Object.entries(strings).forEach(([key, value]) => {
 | 
			
		||||
                    if (targetStrings.hasOwnProperty(key) && !targetStrings[key].startsWith("[UNTRANSLATED]")) {
 | 
			
		||||
                    if (targetStrings.hasOwnProperty(key)) {
 | 
			
		||||
                        fs.writeSync(fileHandle, `    ${key}: \`${targetStrings[key]}\`,\n`);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        fs.writeSync(fileHandle, `    ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
 | 
			
		||||
                    }
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
            } else if (line.length) {
 | 
			
		||||
                fs.writeSync(fileHandle, line + "\n");
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
							
								
								
									
										35
									
								
								src/app.ts
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								src/app.ts
									
									
									
									
									
								
							@ -1,39 +1,32 @@
 | 
			
		||||
import express from "express";
 | 
			
		||||
 | 
			
		||||
import bodyParser from "body-parser";
 | 
			
		||||
import { unknownEndpointHandler } from "./middleware/middleware.ts";
 | 
			
		||||
import { requestLogger } from "./middleware/morgenMiddleware.ts";
 | 
			
		||||
import { errorHandler } from "./middleware/errorHandler.ts";
 | 
			
		||||
import { unknownEndpointHandler } from "@/src/middleware/middleware";
 | 
			
		||||
import { requestLogger } from "@/src/middleware/morgenMiddleware";
 | 
			
		||||
import { errorHandler } from "@/src/middleware/errorHandler";
 | 
			
		||||
 | 
			
		||||
import { apiRouter } from "./routes/api.ts";
 | 
			
		||||
import { cacheRouter } from "./routes/cache.ts";
 | 
			
		||||
import { customRouter } from "./routes/custom.ts";
 | 
			
		||||
import { dynamicController } from "./routes/dynamic.ts";
 | 
			
		||||
import { payRouter } from "./routes/pay.ts";
 | 
			
		||||
import { statsRouter } from "./routes/stats.ts";
 | 
			
		||||
import { webuiRouter } from "./routes/webui.ts";
 | 
			
		||||
import { apiRouter } from "@/src/routes/api";
 | 
			
		||||
import { cacheRouter } from "@/src/routes/cache";
 | 
			
		||||
import { customRouter } from "@/src/routes/custom";
 | 
			
		||||
import { dynamicController } from "@/src/routes/dynamic";
 | 
			
		||||
import { payRouter } from "@/src/routes/pay";
 | 
			
		||||
import { statsRouter } from "@/src/routes/stats";
 | 
			
		||||
import { webuiRouter } from "@/src/routes/webui";
 | 
			
		||||
 | 
			
		||||
const app = express();
 | 
			
		||||
 | 
			
		||||
app.use((req, _res, next) => {
 | 
			
		||||
    // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
 | 
			
		||||
    // The client patch is expected to decrypt it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
 | 
			
		||||
    if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
 | 
			
		||||
    // 38.5.0 introduced "ezip" for encrypted body blobs.
 | 
			
		||||
    // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
 | 
			
		||||
    if (req.headers["content-encoding"] == "ezip") {
 | 
			
		||||
        req.headers["content-encoding"] = undefined;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
 | 
			
		||||
    // U17 sets no Content-Type at all, which Express also doesn't like.
 | 
			
		||||
    if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
 | 
			
		||||
        req.headers["content-type"] = "application/octet-stream";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
app.use(bodyParser.raw());
 | 
			
		||||
app.use(express.json({ limit: "4mb" }));
 | 
			
		||||
app.use(bodyParser.text({ limit: "4mb" }));
 | 
			
		||||
app.use(bodyParser.text());
 | 
			
		||||
app.use(requestLogger);
 | 
			
		||||
 | 
			
		||||
app.use("/api", apiRouter);
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,16 @@
 | 
			
		||||
export const EPOCH = 1734307200_000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be the start of winter in 1999 iteration 0
 | 
			
		||||
 | 
			
		||||
const millisecondsPerSecond = 1000;
 | 
			
		||||
const secondsPerMinute = 60;
 | 
			
		||||
const minutesPerHour = 60;
 | 
			
		||||
const hoursPerDay = 24;
 | 
			
		||||
const daysPerWeek = 7;
 | 
			
		||||
 | 
			
		||||
const unixSecond = millisecondsPerSecond;
 | 
			
		||||
const unixMinute = secondsPerMinute * millisecondsPerSecond;
 | 
			
		||||
const unixHour = unixMinute * minutesPerHour;
 | 
			
		||||
const unixDay = hoursPerDay * unixHour;
 | 
			
		||||
const unixWeek = daysPerWeek * unixDay;
 | 
			
		||||
 | 
			
		||||
export const unixTimesInMs = {
 | 
			
		||||
    second: unixSecond,
 | 
			
		||||
    minute: unixMinute,
 | 
			
		||||
    hour: unixHour,
 | 
			
		||||
    day: unixDay,
 | 
			
		||||
    week: unixWeek
 | 
			
		||||
    day: unixDay
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const abandonLibraryDailyTaskController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -5,11 +5,11 @@ import {
 | 
			
		||||
    hasGuildPermission,
 | 
			
		||||
    removeDojoDeco,
 | 
			
		||||
    removeDojoRoom
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const abortDojoComponentController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -31,13 +31,12 @@ export const abortDojoComponentController: RequestHandler = async (req, res) =>
 | 
			
		||||
 | 
			
		||||
    if (request.DecoId) {
 | 
			
		||||
        removeDojoDeco(guild, request.ComponentId, request.DecoId);
 | 
			
		||||
        await guild.save();
 | 
			
		||||
        res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
			
		||||
    } else {
 | 
			
		||||
        await removeDojoRoom(guild, request.ComponentId);
 | 
			
		||||
        await guild.save();
 | 
			
		||||
        res.json(await getDojoClient(guild, 0));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
    res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAbortDojoComponentRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,8 @@
 | 
			
		||||
import {
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    hasGuildPermission
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const abortDojoComponentDestructionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,19 +1,15 @@
 | 
			
		||||
import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import {
 | 
			
		||||
    createVeiledRivenFingerprint,
 | 
			
		||||
    createUnveiledRivenFingerprint,
 | 
			
		||||
    rivenRawToRealWeighted
 | 
			
		||||
} from "../../helpers/rivenHelper.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { addMods, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getRandomElement } from "../../services/rngService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { addMods, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getRandomElement } from "@/src/services/rngService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const activateRandomModController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "RawUpgrades Upgrades instantFinishRivenChallenge");
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const request = getJSONfromString<IActiveRandomModRequest>(String(req.body));
 | 
			
		||||
    addMods(inventory, [
 | 
			
		||||
        {
 | 
			
		||||
@ -21,10 +17,8 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
 | 
			
		||||
            ItemCount: -1
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
    const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!;
 | 
			
		||||
    const fingerprint = inventory.instantFinishRivenChallenge
 | 
			
		||||
        ? createUnveiledRivenFingerprint(ExportUpgrades[rivenType])
 | 
			
		||||
        : createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
 | 
			
		||||
    const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]);
 | 
			
		||||
    const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
 | 
			
		||||
    const upgradeIndex =
 | 
			
		||||
        inventory.Upgrades.push({
 | 
			
		||||
            ItemType: rivenType,
 | 
			
		||||
 | 
			
		||||
@ -1,60 +0,0 @@
 | 
			
		||||
import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Friendship } from "../../models/friendModel.ts";
 | 
			
		||||
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const addFriendController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = getJSONfromString<IAddFriendRequest>(String(req.body));
 | 
			
		||||
    const promises: Promise<void>[] = [];
 | 
			
		||||
    const newFriends: IFriendInfo[] = [];
 | 
			
		||||
    if (payload.friend == "all") {
 | 
			
		||||
        const [internalFriendships, externalFriendships] = await Promise.all([
 | 
			
		||||
            Friendship.find({ owner: accountId }, "friend"),
 | 
			
		||||
            Friendship.find({ friend: accountId }, "owner")
 | 
			
		||||
        ]);
 | 
			
		||||
        for (const externalFriendship of externalFriendships) {
 | 
			
		||||
            if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
 | 
			
		||||
                promises.push(
 | 
			
		||||
                    Friendship.insertOne({
 | 
			
		||||
                        owner: accountId,
 | 
			
		||||
                        friend: externalFriendship.owner,
 | 
			
		||||
                        Note: externalFriendship.Note // TOVERIFY: Should the note be copied when accepting a friend request?
 | 
			
		||||
                    }) as unknown as Promise<void>
 | 
			
		||||
                );
 | 
			
		||||
                newFriends.push({
 | 
			
		||||
                    _id: toOid(externalFriendship.owner)
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        const externalFriendship = await Friendship.findOne({ owner: payload.friend, friend: accountId }, "Note");
 | 
			
		||||
        if (externalFriendship) {
 | 
			
		||||
            promises.push(
 | 
			
		||||
                Friendship.insertOne({
 | 
			
		||||
                    owner: accountId,
 | 
			
		||||
                    friend: payload.friend,
 | 
			
		||||
                    Note: externalFriendship.Note
 | 
			
		||||
                }) as unknown as Promise<void>
 | 
			
		||||
            );
 | 
			
		||||
            newFriends.push({
 | 
			
		||||
                _id: { $oid: payload.friend }
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (const newFriend of newFriends) {
 | 
			
		||||
        promises.push(addAccountDataToFriendInfo(newFriend));
 | 
			
		||||
        promises.push(addInventoryDataToFriendInfo(newFriend));
 | 
			
		||||
    }
 | 
			
		||||
    await Promise.all(promises);
 | 
			
		||||
    res.json({
 | 
			
		||||
        Friends: newFriends
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddFriendRequest {
 | 
			
		||||
    friend: string; // oid or "all" in which case all=1 is also a query parameter
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
 | 
			
		||||
export const addFriendImageController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Account, Ignore } from "../../models/loginModel.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const addIgnoredUserController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const data = getJSONfromString<IAddIgnoredUserRequest>(String(req.body));
 | 
			
		||||
    const ignoreeAccount = await Account.findOne(
 | 
			
		||||
        { DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
 | 
			
		||||
        "_id"
 | 
			
		||||
    );
 | 
			
		||||
    if (ignoreeAccount) {
 | 
			
		||||
        await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id });
 | 
			
		||||
        res.json({
 | 
			
		||||
            Ignored: {
 | 
			
		||||
                _id: toOid(ignoreeAccount._id),
 | 
			
		||||
                DisplayName: data.playerName
 | 
			
		||||
            } satisfies IFriendInfo
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddIgnoredUserRequest {
 | 
			
		||||
    playerName: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Friendship } from "../../models/friendModel.ts";
 | 
			
		||||
import { Account } from "../../models/loginModel.ts";
 | 
			
		||||
import { addInventoryDataToFriendInfo, areFriendsOfFriends } from "../../services/friendService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const addPendingFriendController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const payload = getJSONfromString<IAddPendingFriendRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const account = await Account.findOne({ DisplayName: payload.friend });
 | 
			
		||||
    if (!account) {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "Settings");
 | 
			
		||||
    if (
 | 
			
		||||
        inventory.Settings?.FriendInvRestriction == "GIFT_MODE_NONE" ||
 | 
			
		||||
        (inventory.Settings?.FriendInvRestriction == "GIFT_MODE_FRIENDS" &&
 | 
			
		||||
            !(await areFriendsOfFriends(account._id, accountId)))
 | 
			
		||||
    ) {
 | 
			
		||||
        res.status(400).send("Friend Invite Restriction");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await Friendship.insertOne({
 | 
			
		||||
        owner: accountId,
 | 
			
		||||
        friend: account._id,
 | 
			
		||||
        Note: payload.message
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const friendInfo: IFriendInfo = {
 | 
			
		||||
        _id: toOid(account._id),
 | 
			
		||||
        DisplayName: account.DisplayName,
 | 
			
		||||
        LastLogin: toMongoDate(account.LastLogin),
 | 
			
		||||
        Note: payload.message
 | 
			
		||||
    };
 | 
			
		||||
    await addInventoryDataToFriendInfo(friendInfo);
 | 
			
		||||
    res.json({
 | 
			
		||||
        Friend: friendInfo
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAddPendingFriendRequest {
 | 
			
		||||
    friend: string;
 | 
			
		||||
    message: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import { getJSONfromString, regexEscape } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { createMessage } from "../../services/inboxService.ts";
 | 
			
		||||
import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const addToAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -75,7 +75,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
 | 
			
		||||
    const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
 | 
			
		||||
    const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
 | 
			
		||||
    const alliance = (await Alliance.findById(req.query.allianceId as string, "Name"))!;
 | 
			
		||||
    const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!;
 | 
			
		||||
    await createMessage(invitedClanOwnerMember.accountId, [
 | 
			
		||||
        {
 | 
			
		||||
            sndr: getSuffixedName(account),
 | 
			
		||||
@ -95,7 +95,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
 | 
			
		||||
            icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
			
		||||
            icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
			
		||||
            contextInfo: alliance._id.toString(),
 | 
			
		||||
            highPriority: true,
 | 
			
		||||
            acceptAction: "ALLIANCE_INVITE",
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,13 @@
 | 
			
		||||
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { Account } from "../../models/loginModel.ts";
 | 
			
		||||
import { addInventoryDataToFriendInfo, areFriends } from "../../services/friendService.ts";
 | 
			
		||||
import { hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { createMessage } from "../../services/inboxService.ts";
 | 
			
		||||
import { getEffectiveAvatarImageType, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
			
		||||
import type { IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { IGuildMemberClient } from "../../types/guildTypes.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -25,18 +22,15 @@ export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const senderAccount = await getAccountForRequest(req);
 | 
			
		||||
        const inventory = await getInventory(account._id.toString(), "Settings");
 | 
			
		||||
        if (
 | 
			
		||||
            inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" ||
 | 
			
		||||
            (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" &&
 | 
			
		||||
                !(await areFriends(account._id, senderAccount._id)))
 | 
			
		||||
        ) {
 | 
			
		||||
        // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
 | 
			
		||||
        if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") {
 | 
			
		||||
            res.status(400).json("Invite restricted");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
 | 
			
		||||
        const senderAccount = await getAccountForRequest(req);
 | 
			
		||||
        if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
 | 
			
		||||
            res.status(400).json("Invalid permission");
 | 
			
		||||
        }
 | 
			
		||||
@ -65,7 +59,7 @@ export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
 | 
			
		||||
                icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
			
		||||
                icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
			
		||||
                contextInfo: payload.GuildId.$oid,
 | 
			
		||||
                highPriority: true,
 | 
			
		||||
                acceptAction: "GUILD_INVITE",
 | 
			
		||||
@ -77,11 +71,10 @@ export const addToGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
        const member: IGuildMemberClient = {
 | 
			
		||||
            _id: { $oid: account._id.toString() },
 | 
			
		||||
            DisplayName: account.DisplayName,
 | 
			
		||||
            LastLogin: toMongoDate(account.LastLogin),
 | 
			
		||||
            Rank: 7,
 | 
			
		||||
            Status: 2
 | 
			
		||||
        };
 | 
			
		||||
        await addInventoryDataToFriendInfo(member);
 | 
			
		||||
        await fillInInventoryDataForGuildMember(member);
 | 
			
		||||
        res.json({ NewMember: member });
 | 
			
		||||
    } else if ("RequestMsg" in payload) {
 | 
			
		||||
        // Player applying to join a clan
 | 
			
		||||
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const adoptPetController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "KubrowPets");
 | 
			
		||||
    const data = getJSONfromString<IAdoptPetRequest>(String(req.body));
 | 
			
		||||
    const details = inventory.KubrowPets.id(data.petId)!.Details!;
 | 
			
		||||
    details.Name = data.name;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        petId: data.petId,
 | 
			
		||||
        newName: data.name
 | 
			
		||||
    } satisfies IAdoptPetResponse);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IAdoptPetRequest {
 | 
			
		||||
    petId: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IAdoptPetResponse {
 | 
			
		||||
    petId: string;
 | 
			
		||||
    newName: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,22 +0,0 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const apartmentController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const personalRooms = await getPersonalRooms(accountId, "Apartment");
 | 
			
		||||
    const response: IApartmentResponse = {};
 | 
			
		||||
    if (req.query.backdrop !== undefined) {
 | 
			
		||||
        response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
 | 
			
		||||
    }
 | 
			
		||||
    if (req.query.soundscape !== undefined) {
 | 
			
		||||
        response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
 | 
			
		||||
    }
 | 
			
		||||
    await personalRooms.save();
 | 
			
		||||
    res.json(response);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IApartmentResponse {
 | 
			
		||||
    NewBackdropItem?: string;
 | 
			
		||||
    NewSoundscapeItem?: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory, addMods } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getInventory, addMods } from "@/src/services/inventoryService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
 | 
			
		||||
export const arcaneCommonController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { colorToShard, combineColors, shardToColor } from "../../helpers/shardHelper.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { colorToShard, combineColors, shardToColor } from "@/src/helpers/shardHelper";
 | 
			
		||||
 | 
			
		||||
export const archonFusionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,11 @@
 | 
			
		||||
import { fromOid, toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "../../helpers/rivenHelper.ts";
 | 
			
		||||
import { addMiscItems, addMods, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "../../services/rngService.ts";
 | 
			
		||||
import type { IUpgradeFromClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type { TRarity } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportBoosterPacks, ExportUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
 | 
			
		||||
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const artifactTransmutationController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -25,11 +24,11 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        payload.Consumed.forEach(upgrade => {
 | 
			
		||||
            inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
 | 
			
		||||
            inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const rawRivenType = getRandomRawRivenType();
 | 
			
		||||
        const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
 | 
			
		||||
        const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
 | 
			
		||||
        const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
 | 
			
		||||
 | 
			
		||||
        const upgradeIndex =
 | 
			
		||||
@ -58,16 +57,12 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
        payload.Consumed.forEach(upgrade => {
 | 
			
		||||
            const meta = ExportUpgrades[upgrade.ItemType];
 | 
			
		||||
            counts[meta.rarity] += upgrade.ItemCount;
 | 
			
		||||
            if (fromOid(upgrade.ItemId) != "000000000000000000000000") {
 | 
			
		||||
                inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) });
 | 
			
		||||
            } else {
 | 
			
		||||
            addMods(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: upgrade.ItemType,
 | 
			
		||||
                    ItemCount: upgrade.ItemCount * -1
 | 
			
		||||
                }
 | 
			
		||||
            ]);
 | 
			
		||||
            }
 | 
			
		||||
            if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
 | 
			
		||||
                forcedPolarity = "AP_ATTACK";
 | 
			
		||||
            } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
 | 
			
		||||
@ -77,15 +72,6 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        let newModType: string | undefined;
 | 
			
		||||
        for (const specialModSet of specialModSets) {
 | 
			
		||||
            if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
 | 
			
		||||
                newModType = getRandomElement(specialModSet);
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!newModType) {
 | 
			
		||||
        // Based on the table on https://wiki.warframe.com/w/Transmutation
 | 
			
		||||
        const weights: Record<TRarity, number> = {
 | 
			
		||||
            COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
 | 
			
		||||
@ -101,9 +87,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
            newModType = getRandomWeightedReward(options, weights)!.uniqueName;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
 | 
			
		||||
        addMods(inventory, [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: newModType,
 | 
			
		||||
@ -129,41 +113,20 @@ const getRandomRawRivenType = (): string => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IArtifactTransmutationRequest {
 | 
			
		||||
    Upgrade: IUpgradeFromClient;
 | 
			
		||||
    Upgrade: IAgnosticUpgradeClient;
 | 
			
		||||
    LevelDiff: number;
 | 
			
		||||
    Consumed: IUpgradeFromClient[];
 | 
			
		||||
    Consumed: IAgnosticUpgradeClient[];
 | 
			
		||||
    Cost: number;
 | 
			
		||||
    FusionPointCost: number;
 | 
			
		||||
    RivenTransmute?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const specialModSets: string[][] = [
 | 
			
		||||
    [
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
 | 
			
		||||
        "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
 | 
			
		||||
    ]
 | 
			
		||||
];
 | 
			
		||||
interface IAgnosticUpgradeClient {
 | 
			
		||||
    ItemType: string;
 | 
			
		||||
    ItemId: IOid;
 | 
			
		||||
    FromSKU: boolean;
 | 
			
		||||
    UpgradeFingerprint: string;
 | 
			
		||||
    PendingRerollFingerprint: string;
 | 
			
		||||
    ItemCount: number;
 | 
			
		||||
    LastAdded: IOid;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type { IInventoryClient, IUpgradeClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { addMods, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { IInventoryClient, IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { addMods, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
 | 
			
		||||
export const artifactsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -24,6 +24,7 @@ export const artifactsController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    if (itemIndex !== -1) {
 | 
			
		||||
        Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
 | 
			
		||||
        inventory.markModified(`Upgrades.${itemIndex}.UpgradeFingerprint`);
 | 
			
		||||
    } else {
 | 
			
		||||
        itemIndex =
 | 
			
		||||
            Upgrades.push({
 | 
			
		||||
@ -34,10 +35,10 @@ export const artifactsController: RequestHandler = async (req, res) => {
 | 
			
		||||
        addMods(inventory, [{ ItemType, ItemCount: -1 }]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!inventory.infiniteCredits) {
 | 
			
		||||
    if (!config.infiniteCredits) {
 | 
			
		||||
        inventory.RegularCredits -= Cost;
 | 
			
		||||
    }
 | 
			
		||||
    if (!inventory.infiniteEndo) {
 | 
			
		||||
    if (!config.infiniteEndo) {
 | 
			
		||||
        inventory.FusionPoints -= FusionPointCost;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -58,7 +59,6 @@ export const artifactsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.send(itemId);
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IArtifactsRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { GuildAd } from "../../models/guildModel.ts";
 | 
			
		||||
import { getGuildForRequestEx, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { GuildAd } from "@/src/models/guildModel";
 | 
			
		||||
import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,10 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import {
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    hasGuildPermission
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { IDojoComponentDatabase } from "../../types/guildTypes.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
 | 
			
		||||
export const changeDojoRootController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -21,12 +15,6 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
 | 
			
		||||
        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> = {};
 | 
			
		||||
    guild.DojoComponents.forEach(x => {
 | 
			
		||||
        idToNode[x._id.toString()] = {
 | 
			
		||||
@ -55,13 +43,23 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
 | 
			
		||||
    newRoot.component.pp = 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];
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    const idMap: Record<string, Types.ObjectId> = {};
 | 
			
		||||
    while (stack.length != 0) {
 | 
			
		||||
        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));
 | 
			
		||||
    }
 | 
			
		||||
    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));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getGuildForRequest, hasGuildPermissionEx } from "../../services/guildService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getGuildForRequest, hasGuildPermissionEx } from "@/src/services/guildService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const changeGuildRankController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,16 @@
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const checkDailyMissionBonusController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const today = Math.trunc(Date.now() / 86400000) * 86400;
 | 
			
		||||
    if (account.DailyFirstWinDate != today) {
 | 
			
		||||
        res.send("DailyMissionBonus:1-DailyPVPWinBonus:1\n");
 | 
			
		||||
    } else {
 | 
			
		||||
        res.send("DailyMissionBonus:0-DailyPVPWinBonus:1\n");
 | 
			
		||||
    }
 | 
			
		||||
const checkDailyMissionBonusController: RequestHandler = (_req, res) => {
 | 
			
		||||
    const data = Buffer.from([
 | 
			
		||||
        0x44, 0x61, 0x69, 0x6c, 0x79, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73, 0x3a,
 | 
			
		||||
        0x31, 0x2d, 0x44, 0x61, 0x69, 0x6c, 0x79, 0x50, 0x56, 0x50, 0x57, 0x69, 0x6e, 0x42, 0x6f, 0x6e, 0x75, 0x73,
 | 
			
		||||
        0x3a, 0x31, 0x0a
 | 
			
		||||
    ]);
 | 
			
		||||
    res.writeHead(200, {
 | 
			
		||||
        "Content-Type": "text/html",
 | 
			
		||||
        "Content-Length": data.length
 | 
			
		||||
    });
 | 
			
		||||
    res.end(data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { checkDailyMissionBonusController };
 | 
			
		||||
 | 
			
		||||
@ -1,53 +1,37 @@
 | 
			
		||||
//this is a controller for the claimCompletedRecipe route
 | 
			
		||||
//it will claim a recipe for the user
 | 
			
		||||
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import { getRecipe } from "../../services/itemDataService.ts";
 | 
			
		||||
import type { IOidWithLegacySupport } from "../../types/commonTypes.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { TAccountDocument } from "../../services/loginService.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { getRecipe } from "@/src/services/itemDataService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import {
 | 
			
		||||
    getInventory,
 | 
			
		||||
    updateCurrency,
 | 
			
		||||
    addItem,
 | 
			
		||||
    addRecipes,
 | 
			
		||||
    occupySlot,
 | 
			
		||||
    combineInventoryChanges,
 | 
			
		||||
    addKubrowPetPrint,
 | 
			
		||||
    addPowerSuit,
 | 
			
		||||
    addEquipment
 | 
			
		||||
} from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import type { IRecipe } from "warframe-public-export-plus";
 | 
			
		||||
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
 | 
			
		||||
import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
 | 
			
		||||
    combineInventoryChanges
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
 | 
			
		||||
interface IClaimCompletedRecipeRequest {
 | 
			
		||||
    RecipeIds: IOidWithLegacySupport[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IClaimCompletedRecipeResponse {
 | 
			
		||||
    InventoryChanges: IInventoryChanges;
 | 
			
		||||
    BrandedSuits?: IOidWithLegacySupport[];
 | 
			
		||||
    RecipeIds: IOid[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(account._id.toString());
 | 
			
		||||
    const resp: IClaimCompletedRecipeResponse = {
 | 
			
		||||
        InventoryChanges: {}
 | 
			
		||||
    };
 | 
			
		||||
    for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
 | 
			
		||||
        const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    if (!accountId) throw new Error("no account id");
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
 | 
			
		||||
    if (!pendingRecipe) {
 | 
			
		||||
            throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
 | 
			
		||||
        throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //check recipe is indeed ready to be completed
 | 
			
		||||
@ -63,223 +47,9 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (req.query.cancel) {
 | 
			
		||||
            const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
            await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json(resp);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const claimCompletedRecipe = async (
 | 
			
		||||
    account: TAccountDocument,
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    recipe: IRecipe,
 | 
			
		||||
    pendingRecipe: IPendingRecipeDatabase,
 | 
			
		||||
    resp: IClaimCompletedRecipeResponse,
 | 
			
		||||
    rush: any
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    logger.debug("Claiming Recipe", { recipe, pendingRecipe });
 | 
			
		||||
 | 
			
		||||
    if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
 | 
			
		||||
        inventory.PendingSpectreLoadouts ??= [];
 | 
			
		||||
        inventory.SpectreLoadouts ??= [];
 | 
			
		||||
 | 
			
		||||
        const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
 | 
			
		||||
        if (pendingLoadoutIndex != -1) {
 | 
			
		||||
            const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
 | 
			
		||||
            if (loadoutIndex != -1) {
 | 
			
		||||
                inventory.SpectreLoadouts.splice(loadoutIndex, 1);
 | 
			
		||||
            }
 | 
			
		||||
            logger.debug(
 | 
			
		||||
                "moving spectre loadout from pending to active",
 | 
			
		||||
                inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
 | 
			
		||||
            );
 | 
			
		||||
            inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
 | 
			
		||||
            inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
 | 
			
		||||
        }
 | 
			
		||||
    } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
 | 
			
		||||
        inventory.BrandedSuits!.splice(
 | 
			
		||||
            inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
 | 
			
		||||
            1
 | 
			
		||||
        );
 | 
			
		||||
        resp.BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (recipe.consumeOnUse) {
 | 
			
		||||
        addRecipes(inventory, [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: pendingRecipe.ItemType,
 | 
			
		||||
                ItemCount: -1
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (rush) {
 | 
			
		||||
        const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
 | 
			
		||||
        const start = end - recipe.buildTime;
 | 
			
		||||
        const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
 | 
			
		||||
        const progress = secondsElapsed / recipe.buildTime;
 | 
			
		||||
        logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
 | 
			
		||||
        const cost =
 | 
			
		||||
            progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
 | 
			
		||||
        combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
 | 
			
		||||
        const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
			
		||||
        if (pet.Details!.HatchDate!.getTime() > Date.now()) {
 | 
			
		||||
            pet.Details!.HatchDate = new Date();
 | 
			
		||||
        }
 | 
			
		||||
        let canSetActive = true;
 | 
			
		||||
        for (const pet of inventory.KubrowPets) {
 | 
			
		||||
            if (pet.Details!.Status == Status.StatusAvailable) {
 | 
			
		||||
                canSetActive = false;
 | 
			
		||||
                break;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
 | 
			
		||||
    } else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
 | 
			
		||||
        const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
 | 
			
		||||
        addKubrowPetPrint(inventory, pet, resp.InventoryChanges);
 | 
			
		||||
    } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
			
		||||
        if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
 | 
			
		||||
            // Quite the special case here...
 | 
			
		||||
            // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
 | 
			
		||||
            // Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
 | 
			
		||||
 | 
			
		||||
            const umbraModA = (
 | 
			
		||||
                await addItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
 | 
			
		||||
                    1,
 | 
			
		||||
                    false,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    `{"lvl":5}`
 | 
			
		||||
                )
 | 
			
		||||
            ).Upgrades![0];
 | 
			
		||||
            const umbraModB = (
 | 
			
		||||
                await addItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
 | 
			
		||||
                    1,
 | 
			
		||||
                    false,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    `{"lvl":5}`
 | 
			
		||||
                )
 | 
			
		||||
            ).Upgrades![0];
 | 
			
		||||
            const umbraModC = (
 | 
			
		||||
                await addItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
 | 
			
		||||
                    1,
 | 
			
		||||
                    false,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    `{"lvl":5}`
 | 
			
		||||
                )
 | 
			
		||||
            ).Upgrades![0];
 | 
			
		||||
            const sacrificeModA = (
 | 
			
		||||
                await addItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
 | 
			
		||||
                    1,
 | 
			
		||||
                    false,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    `{"lvl":5}`
 | 
			
		||||
                )
 | 
			
		||||
            ).Upgrades![0];
 | 
			
		||||
            const sacrificeModB = (
 | 
			
		||||
                await addItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
 | 
			
		||||
                    1,
 | 
			
		||||
                    false,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    `{"lvl":5}`
 | 
			
		||||
                )
 | 
			
		||||
            ).Upgrades![0];
 | 
			
		||||
            resp.InventoryChanges.Upgrades ??= [];
 | 
			
		||||
            resp.InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
 | 
			
		||||
 | 
			
		||||
            await addPowerSuit(
 | 
			
		||||
                inventory,
 | 
			
		||||
                "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
			
		||||
                {
 | 
			
		||||
                    Configs: [
 | 
			
		||||
                        {
 | 
			
		||||
                            Upgrades: [
 | 
			
		||||
                                "",
 | 
			
		||||
                                "",
 | 
			
		||||
                                "",
 | 
			
		||||
                                "",
 | 
			
		||||
                                "",
 | 
			
		||||
                                umbraModA.ItemId.$oid,
 | 
			
		||||
                                umbraModB.ItemId.$oid,
 | 
			
		||||
                                umbraModC.ItemId.$oid
 | 
			
		||||
                            ]
 | 
			
		||||
                        }
 | 
			
		||||
                    ],
 | 
			
		||||
                    XP: 900_000,
 | 
			
		||||
                    Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
			
		||||
                },
 | 
			
		||||
                resp.InventoryChanges
 | 
			
		||||
            );
 | 
			
		||||
            inventory.XPInfo.push({
 | 
			
		||||
                ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
 | 
			
		||||
                XP: 900_000
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            addEquipment(
 | 
			
		||||
                inventory,
 | 
			
		||||
                "Melee",
 | 
			
		||||
                "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
			
		||||
                {
 | 
			
		||||
                    Configs: [
 | 
			
		||||
                        { Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
 | 
			
		||||
                    ],
 | 
			
		||||
                    XP: 450_000,
 | 
			
		||||
                    Features: EquipmentFeatures.DOUBLE_CAPACITY
 | 
			
		||||
                },
 | 
			
		||||
                resp.InventoryChanges
 | 
			
		||||
            );
 | 
			
		||||
            inventory.XPInfo.push({
 | 
			
		||||
                ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
 | 
			
		||||
                XP: 450_000
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            combineInventoryChanges(
 | 
			
		||||
                resp.InventoryChanges,
 | 
			
		||||
                await addItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    recipe.resultType,
 | 
			
		||||
                    recipe.num,
 | 
			
		||||
                    false,
 | 
			
		||||
                    undefined,
 | 
			
		||||
                    pendingRecipe.TargetFingerprint
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
        inventory.claimingBlueprintRefundsIngredients &&
 | 
			
		||||
        recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
 | 
			
		||||
    ) {
 | 
			
		||||
        await refundRecipeIngredients(inventory, resp.InventoryChanges, recipe, pendingRecipe);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const refundRecipeIngredients = async (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    inventoryChanges: IInventoryChanges,
 | 
			
		||||
    recipe: IRecipe,
 | 
			
		||||
    pendingRecipe: IPendingRecipeDatabase
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
 | 
			
		||||
        const inventoryChanges: IInventoryChanges = {
 | 
			
		||||
            ...updateCurrency(inventory, recipe.buildPrice * -1, false)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const equipmentIngredients = new Set();
 | 
			
		||||
        for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
 | 
			
		||||
@ -304,4 +74,66 @@ const refundRecipeIngredients = async (
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.debug("Claiming Recipe", { recipe, pendingRecipe });
 | 
			
		||||
 | 
			
		||||
        if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
 | 
			
		||||
            inventory.PendingSpectreLoadouts ??= [];
 | 
			
		||||
            inventory.SpectreLoadouts ??= [];
 | 
			
		||||
 | 
			
		||||
            const pendingLoadoutIndex = inventory.PendingSpectreLoadouts.findIndex(
 | 
			
		||||
                x => x.ItemType == recipe.resultType
 | 
			
		||||
            );
 | 
			
		||||
            if (pendingLoadoutIndex != -1) {
 | 
			
		||||
                const loadoutIndex = inventory.SpectreLoadouts.findIndex(x => x.ItemType == recipe.resultType);
 | 
			
		||||
                if (loadoutIndex != -1) {
 | 
			
		||||
                    inventory.SpectreLoadouts.splice(loadoutIndex, 1);
 | 
			
		||||
                }
 | 
			
		||||
                logger.debug(
 | 
			
		||||
                    "moving spectre loadout from pending to active",
 | 
			
		||||
                    inventory.toJSON().PendingSpectreLoadouts![pendingLoadoutIndex]
 | 
			
		||||
                );
 | 
			
		||||
                inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
 | 
			
		||||
                inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
 | 
			
		||||
            }
 | 
			
		||||
        } else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
 | 
			
		||||
            inventory.BrandedSuits!.splice(
 | 
			
		||||
                inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
 | 
			
		||||
                1
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let InventoryChanges = {};
 | 
			
		||||
        if (recipe.consumeOnUse) {
 | 
			
		||||
            addRecipes(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: pendingRecipe.ItemType,
 | 
			
		||||
                    ItemCount: -1
 | 
			
		||||
                }
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
        if (req.query.rush) {
 | 
			
		||||
            const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
 | 
			
		||||
            const start = end - recipe.buildTime;
 | 
			
		||||
            const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
 | 
			
		||||
            const progress = secondsElapsed / recipe.buildTime;
 | 
			
		||||
            logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
 | 
			
		||||
            const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
 | 
			
		||||
            InventoryChanges = {
 | 
			
		||||
                ...InventoryChanges,
 | 
			
		||||
                ...updateCurrency(inventory, cost, true)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        if (recipe.secretIngredientAction != "SIA_UNBRAND") {
 | 
			
		||||
            InventoryChanges = {
 | 
			
		||||
                ...InventoryChanges,
 | 
			
		||||
                ...(await addItem(inventory, recipe.resultType, recipe.num, false))
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({ InventoryChanges });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { combineInventoryChanges, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportChallenges } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const claimJunctionChallengeRewardController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const data = getJSONfromString<IClaimJunctionChallengeRewardRequest>(String(req.body));
 | 
			
		||||
    const challengeProgress = inventory.ChallengeProgress.find(x => x.Name == data.Challenge)!;
 | 
			
		||||
    if (challengeProgress.ReceivedJunctionReward) {
 | 
			
		||||
        throw new Error(`attempt to double-claim junction reward`);
 | 
			
		||||
    }
 | 
			
		||||
    challengeProgress.ReceivedJunctionReward = true;
 | 
			
		||||
    inventory.ClaimedJunctionChallengeRewards ??= [];
 | 
			
		||||
    inventory.ClaimedJunctionChallengeRewards.push(data.Challenge);
 | 
			
		||||
    const challengeMeta = Object.entries(ExportChallenges).find(arr => arr[0].endsWith("/" + data.Challenge))![1];
 | 
			
		||||
    const inventoryChanges = {};
 | 
			
		||||
    for (const reward of challengeMeta.countedRewards!) {
 | 
			
		||||
        combineInventoryChanges(
 | 
			
		||||
            inventoryChanges,
 | 
			
		||||
            (await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount)).InventoryChanges
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        inventoryChanges: inventoryChanges // Yeah, it's "inventoryChanges" in the response here.
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IClaimJunctionChallengeRewardRequest {
 | 
			
		||||
    Challenge: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const claimLibraryDailyTaskRewardController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -17,7 +17,7 @@ export const claimLibraryDailyTaskRewardController: RequestHandler = async (req,
 | 
			
		||||
    }
 | 
			
		||||
    syndicate.Standing += rewardStanding;
 | 
			
		||||
 | 
			
		||||
    addFusionPoints(inventory, 80 * rewardQuantity);
 | 
			
		||||
    inventory.FusionPoints += 80 * rewardQuantity;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const clearDialogueHistoryController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
// example req.body: {"NewEpisodeReward":true,"crossPlaySetting":"ENABLED"}
 | 
			
		||||
export const clearNewEpisodeRewardController: RequestHandler = (_req, res) => {
 | 
			
		||||
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
import { checkCalendarAutoAdvance, getCalendarProgress, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
			
		||||
import { getWorldState } from "../../services/worldStateService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER
 | 
			
		||||
export const completeCalendarEventController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const calendarProgress = getCalendarProgress(inventory);
 | 
			
		||||
    const currentSeason = getWorldState().KnownCalendarSeasons[0];
 | 
			
		||||
    let inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
 | 
			
		||||
    const day = currentSeason.Days[dayIndex];
 | 
			
		||||
    if (day.events.length != 0) {
 | 
			
		||||
        if (day.events[0].type == "CET_CHALLENGE") {
 | 
			
		||||
            throw new Error(`completeCalendarEvent should not be used for challenges`);
 | 
			
		||||
        }
 | 
			
		||||
        const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
 | 
			
		||||
        if (selection.type == "CET_REWARD") {
 | 
			
		||||
            inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
 | 
			
		||||
        } else if (selection.type == "CET_UPGRADE") {
 | 
			
		||||
            calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
 | 
			
		||||
        } else if (selection.type != "CET_PLOT") {
 | 
			
		||||
            throw new Error(`unexpected selection type: ${selection.type}`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
 | 
			
		||||
    checkCalendarAutoAdvance(inventory, currentSeason);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges,
 | 
			
		||||
        CalendarProgress: inventory.CalendarProgress
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { IVeiledRivenFingerprint } from "../../helpers/rivenHelper.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper";
 | 
			
		||||
import { ExportUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -26,11 +27,10 @@ export const completeRandomModChallengeController: RequestHandler = async (req,
 | 
			
		||||
        inventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Complete the riven challenge
 | 
			
		||||
    // Update riven fingerprint to a randomised unveiled state
 | 
			
		||||
    const upgrade = inventory.Upgrades.id(request.ItemId)!;
 | 
			
		||||
    const fp = JSON.parse(upgrade.UpgradeFingerprint!) as IVeiledRivenFingerprint;
 | 
			
		||||
    fp.challenge.Progress = fp.challenge.Required;
 | 
			
		||||
    upgrade.UpgradeFingerprint = JSON.stringify(fp);
 | 
			
		||||
    const meta = ExportUpgrades[upgrade.ItemType];
 | 
			
		||||
    upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta));
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAllianceClient } from "../../services/guildService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAllianceClient } from "@/src/services/guildService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const confirmAllianceInvitationController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,12 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { Account } from "../../models/loginModel.ts";
 | 
			
		||||
import {
 | 
			
		||||
    deleteGuild,
 | 
			
		||||
    getGuildClient,
 | 
			
		||||
    giveClanKey,
 | 
			
		||||
    hasGuildPermission,
 | 
			
		||||
    removeDojoKeyItems
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
 | 
			
		||||
import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
 | 
			
		||||
// GET request: A player accepting an invite they got in their inbox.
 | 
			
		||||
@ -47,7 +41,14 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
 | 
			
		||||
        // Update inventory of new member
 | 
			
		||||
        const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
			
		||||
        giveClanKey(inventory, inventoryChanges);
 | 
			
		||||
        const recipeChanges = [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                ItemCount: 1
 | 
			
		||||
            }
 | 
			
		||||
        ];
 | 
			
		||||
        addRecipes(inventory, recipeChanges);
 | 
			
		||||
        combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
        const guild = (await Guild.findById(req.query.clanId as string))!;
 | 
			
		||||
@ -62,7 +63,7 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
 | 
			
		||||
        await guild.save();
 | 
			
		||||
 | 
			
		||||
        res.json({
 | 
			
		||||
            ...(await getGuildClient(guild, account)),
 | 
			
		||||
            ...(await getGuildClient(guild, account._id.toString())),
 | 
			
		||||
            InventoryChanges: inventoryChanges
 | 
			
		||||
        });
 | 
			
		||||
    } else {
 | 
			
		||||
@ -95,9 +96,14 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req,
 | 
			
		||||
        await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
 | 
			
		||||
 | 
			
		||||
        // Update inventory of new member
 | 
			
		||||
        const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
        const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes");
 | 
			
		||||
        inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
 | 
			
		||||
        giveClanKey(inventory);
 | 
			
		||||
        addRecipes(inventory, [
 | 
			
		||||
            {
 | 
			
		||||
                ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                ItemCount: 1
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
 | 
			
		||||
        // Add join to clan log
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,11 @@
 | 
			
		||||
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { checkClanAscensionHasRequiredContributors } from "../../services/guildService.ts";
 | 
			
		||||
import { addFusionPoints, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
 | 
			
		||||
export const contributeGuildClassController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -30,13 +31,49 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
 | 
			
		||||
 | 
			
		||||
    guild.CeremonyContributors.push(new Types.ObjectId(accountId));
 | 
			
		||||
 | 
			
		||||
    await checkClanAscensionHasRequiredContributors(guild);
 | 
			
		||||
    // Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
 | 
			
		||||
    if (guild.CeremonyContributors.length == payload.RequiredContributors) {
 | 
			
		||||
        guild.Class = guild.CeremonyClass!;
 | 
			
		||||
        guild.CeremonyClass = undefined;
 | 
			
		||||
        guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
 | 
			
		||||
        if (!config.fastClanAscension) {
 | 
			
		||||
            // Send message to all active guild members
 | 
			
		||||
            const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
 | 
			
		||||
            for (const member of members) {
 | 
			
		||||
                // somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
 | 
			
		||||
                await createMessage(member.accountId, [
 | 
			
		||||
                    {
 | 
			
		||||
                        sndr: guild.Name,
 | 
			
		||||
                        msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
 | 
			
		||||
                        arg: [
 | 
			
		||||
                            {
 | 
			
		||||
                                Key: "RESETDATE",
 | 
			
		||||
                                Tag:
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCMonth() +
 | 
			
		||||
                                    "/" +
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCDate() +
 | 
			
		||||
                                    "/" +
 | 
			
		||||
                                    (guild.CeremonyResetDate.getUTCFullYear() % 100) +
 | 
			
		||||
                                    " " +
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
 | 
			
		||||
                                    ":" +
 | 
			
		||||
                                    guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
 | 
			
		||||
                            }
 | 
			
		||||
                        ],
 | 
			
		||||
                        sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
 | 
			
		||||
                        icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
 | 
			
		||||
                        highPriority: true
 | 
			
		||||
                    }
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await guild.save();
 | 
			
		||||
 | 
			
		||||
    // Either way, endo is given to the contributor.
 | 
			
		||||
    const inventory = await getInventory(accountId, "FusionPoints");
 | 
			
		||||
    addFusionPoints(inventory, guild.CeremonyEndo!);
 | 
			
		||||
    inventory.FusionPoints += guild.CeremonyEndo!;
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import type { TGuildDatabaseDocument } from "../../models/guildModel.ts";
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import {
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
@ -9,15 +8,14 @@ import {
 | 
			
		||||
    processDojoBuildMaterialsGathered,
 | 
			
		||||
    scaleRequiredCount,
 | 
			
		||||
    setDojoRoomLogFunded
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { addMiscItems, getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IDojoContributable, IGuildMemberDatabase } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type { IDojoBuild } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IDojoContributable, IGuildMemberDatabase } from "@/src/types/guildTypes";
 | 
			
		||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
interface IContributeToDojoComponentRequest {
 | 
			
		||||
    ComponentId: string;
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,10 @@
 | 
			
		||||
import type { TGuildDatabaseDocument, TGuildMemberDatabaseDocument } from "../../models/guildModel.ts";
 | 
			
		||||
import { Alliance, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import {
 | 
			
		||||
    Alliance,
 | 
			
		||||
    Guild,
 | 
			
		||||
    GuildMember,
 | 
			
		||||
    TGuildDatabaseDocument,
 | 
			
		||||
    TGuildMemberDatabaseDocument
 | 
			
		||||
} from "@/src/models/guildModel";
 | 
			
		||||
import {
 | 
			
		||||
    addGuildMemberMiscItemContribution,
 | 
			
		||||
    addGuildMemberShipDecoContribution,
 | 
			
		||||
@ -7,18 +12,17 @@ import {
 | 
			
		||||
    addVaultMiscItems,
 | 
			
		||||
    addVaultShipDecos,
 | 
			
		||||
    getGuildForRequestEx
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import {
 | 
			
		||||
    addFusionTreasures,
 | 
			
		||||
    addMiscItems,
 | 
			
		||||
    addShipDecorations,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    updateCurrency
 | 
			
		||||
} from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { ITypeCount } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { IFusionTreasure, IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
} from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const contributeToVaultController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAllianceClient } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAllianceClient } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const createAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,16 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { createUniqueClanName, getGuildClient, giveClanKey } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { sendWsBroadcastTo } from "../../services/wsService.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
 | 
			
		||||
import { addRecipes, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
 | 
			
		||||
export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
 | 
			
		||||
    if (inventory.GuildId) {
 | 
			
		||||
        const guild = await Guild.findById(inventory.GuildId);
 | 
			
		||||
        if (guild) {
 | 
			
		||||
            res.json({
 | 
			
		||||
                ...(await getGuildClient(guild, account))
 | 
			
		||||
            });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove pending applications for this account
 | 
			
		||||
    await GuildMember.deleteMany({ accountId: account._id, status: 1 });
 | 
			
		||||
    await GuildMember.deleteMany({ accountId, status: 1 });
 | 
			
		||||
 | 
			
		||||
    // Create guild on database
 | 
			
		||||
    const guild = new Guild({
 | 
			
		||||
@ -33,22 +20,33 @@ export const createGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
 | 
			
		||||
    // Create guild member on database
 | 
			
		||||
    await GuildMember.insertOne({
 | 
			
		||||
        accountId: account._id,
 | 
			
		||||
        accountId: accountId,
 | 
			
		||||
        guildId: guild._id,
 | 
			
		||||
        status: 0,
 | 
			
		||||
        rank: 0
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId Recipes");
 | 
			
		||||
    inventory.GuildId = guild._id;
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    giveClanKey(inventory, inventoryChanges);
 | 
			
		||||
    addRecipes(inventory, [
 | 
			
		||||
        {
 | 
			
		||||
            ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
            ItemCount: 1
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
        ...(await getGuildClient(guild, account)),
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
        ...(await getGuildClient(guild, accountId)),
 | 
			
		||||
        InventoryChanges: {
 | 
			
		||||
            Recipes: [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICreateGuildRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,12 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
 | 
			
		||||
export const creditsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const inventory = (
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            getAccountIdForRequest(req),
 | 
			
		||||
            getInventory(
 | 
			
		||||
                req.query.accountId as string,
 | 
			
		||||
                "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits infiniteCredits infinitePlatinum"
 | 
			
		||||
            )
 | 
			
		||||
        ])
 | 
			
		||||
    )[1];
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId, "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits");
 | 
			
		||||
 | 
			
		||||
    const response = {
 | 
			
		||||
        RegularCredits: inventory.RegularCredits,
 | 
			
		||||
@ -20,10 +15,10 @@ export const creditsController: RequestHandler = async (req, res) => {
 | 
			
		||||
        PremiumCredits: inventory.PremiumCredits
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (inventory.infiniteCredits) {
 | 
			
		||||
    if (config.infiniteCredits) {
 | 
			
		||||
        response.RegularCredits = 999999999;
 | 
			
		||||
    }
 | 
			
		||||
    if (inventory.infinitePlatinum) {
 | 
			
		||||
    if (config.infinitePlatinum) {
 | 
			
		||||
        response.PremiumCreditsFree = 0;
 | 
			
		||||
        response.PremiumCredits = 999999999;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,54 +0,0 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { ICrewMemberClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
 | 
			
		||||
export const crewMembersController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "CrewMembers NemesisHistory");
 | 
			
		||||
    const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
 | 
			
		||||
    if (data.crewMember.SecondInCommand) {
 | 
			
		||||
        clearOnCall(inventory);
 | 
			
		||||
    }
 | 
			
		||||
    if (data.crewMember.ItemId.$oid == "000000000000000000000000") {
 | 
			
		||||
        const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!;
 | 
			
		||||
        convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand;
 | 
			
		||||
    } else {
 | 
			
		||||
        const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
 | 
			
		||||
        dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
 | 
			
		||||
        dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
 | 
			
		||||
        dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
 | 
			
		||||
        dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
 | 
			
		||||
        dbCrewMember.Configs = data.crewMember.Configs;
 | 
			
		||||
        dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        crewMemberId: data.crewMember.ItemId.$oid,
 | 
			
		||||
        NemesisFingerprint: data.crewMember.NemesisFingerprint
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICrewMembersRequest {
 | 
			
		||||
    crewMember: ICrewMemberClient;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
 | 
			
		||||
    for (const cm of inventory.CrewMembers) {
 | 
			
		||||
        if (cm.SecondInCommand) {
 | 
			
		||||
            cm.SecondInCommand = false;
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (inventory.NemesisHistory) {
 | 
			
		||||
        for (const cm of inventory.NemesisHistory) {
 | 
			
		||||
            if (cm.SecondInCommand) {
 | 
			
		||||
                cm.SecondInCommand = false;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -1,107 +0,0 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { ICrewShipComponentFingerprint } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const crewShipFusionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
 | 
			
		||||
    const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
 | 
			
		||||
    const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
 | 
			
		||||
    const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
 | 
			
		||||
    const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
 | 
			
		||||
    const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
 | 
			
		||||
    const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
 | 
			
		||||
    // Charge partial repair cost if fusing with an identified but unrepaired part
 | 
			
		||||
    if (payload.SourceRecipe) {
 | 
			
		||||
        const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
 | 
			
		||||
        updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
 | 
			
		||||
        const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
 | 
			
		||||
        addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        inventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove inferior item
 | 
			
		||||
    if (payload.SourceRecipe) {
 | 
			
		||||
        inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
 | 
			
		||||
        inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
 | 
			
		||||
    } else {
 | 
			
		||||
        const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
 | 
			
		||||
        inventory[category].pull({ _id: inferiorId.$oid });
 | 
			
		||||
        inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
 | 
			
		||||
        freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
			
		||||
        inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Upgrade superior item
 | 
			
		||||
    const superiorItem = tierA < tierB ? itemB : itemA;
 | 
			
		||||
    const inferiorItem = tierA < tierB ? itemA : itemB;
 | 
			
		||||
    const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
 | 
			
		||||
        superiorItem.UpgradeFingerprint!
 | 
			
		||||
    ) as ICrewShipComponentFingerprint;
 | 
			
		||||
    const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
 | 
			
		||||
        ? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
 | 
			
		||||
        : { compat: "", buffs: [] };
 | 
			
		||||
    if (isWeapon) {
 | 
			
		||||
        for (let i = 0; i != fingerprint.buffs.length; ++i) {
 | 
			
		||||
            const buffA = fingerprint.buffs[i];
 | 
			
		||||
            const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
 | 
			
		||||
            const fvalA = buffA.Value / 0x3fffffff;
 | 
			
		||||
            const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
 | 
			
		||||
            const percA = 0.3 + fvalA * (0.6 - 0.3);
 | 
			
		||||
            const percB = 0.3 + fvalB * (0.6 - 0.3);
 | 
			
		||||
            const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
 | 
			
		||||
            const newFval = (newPerc - 0.3) / (0.6 - 0.3);
 | 
			
		||||
            buffA.Value = Math.trunc(newFval * 0x3fffffff);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
 | 
			
		||||
        const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
 | 
			
		||||
        for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
 | 
			
		||||
            const buffA = fingerprint.buffs[i];
 | 
			
		||||
            const buffB = inferiorFingerprint.buffs[i];
 | 
			
		||||
            const fvalA = buffA.Value / 0x3fffffff;
 | 
			
		||||
            const fvalB = buffB.Value / 0x3fffffff;
 | 
			
		||||
            const rangeA = superiorMeta[i].range;
 | 
			
		||||
            const rangeB = inferiorMeta[i].range;
 | 
			
		||||
            const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
 | 
			
		||||
            const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
 | 
			
		||||
            const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
 | 
			
		||||
            const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
 | 
			
		||||
            buffA.Value = Math.trunc(newFval * 0x3fffffff);
 | 
			
		||||
        }
 | 
			
		||||
        if (inferiorFingerprint.SubroutineIndex !== undefined) {
 | 
			
		||||
            const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
 | 
			
		||||
            if (!useSuperiorSubroutine) {
 | 
			
		||||
                fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
 | 
			
		||||
    inventoryChanges[category] = [superiorItem.toJSON() as any];
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICrewShipFusionRequest {
 | 
			
		||||
    PartA: IOid;
 | 
			
		||||
    PartB: IOid;
 | 
			
		||||
    SourceRecipe: string;
 | 
			
		||||
    UseSubroutineA: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02];
 | 
			
		||||
@ -1,87 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
    addCrewShipSalvagedWeaponSkin,
 | 
			
		||||
    addCrewShipRawSalvage,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    addEquipment
 | 
			
		||||
} from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type {
 | 
			
		||||
    ICrewShipComponentFingerprint,
 | 
			
		||||
    IInnateDamageFingerprint
 | 
			
		||||
} from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { getRandomInt } from "../../services/rngService.ts";
 | 
			
		||||
import type { IFingerprintStat } from "../../helpers/rivenHelper.ts";
 | 
			
		||||
import type { IEquipmentDatabase } from "../../types/equipmentTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(
 | 
			
		||||
        accountId,
 | 
			
		||||
        "CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage"
 | 
			
		||||
    );
 | 
			
		||||
    const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    if (payload.ItemType in ExportCustoms) {
 | 
			
		||||
        const meta = ExportCustoms[payload.ItemType];
 | 
			
		||||
        let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] };
 | 
			
		||||
        if (meta.subroutines) {
 | 
			
		||||
            upgradeFingerprint = {
 | 
			
		||||
                SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1),
 | 
			
		||||
                ...upgradeFingerprint
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        for (const upgrade of meta.randomisedUpgrades!) {
 | 
			
		||||
            upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
 | 
			
		||||
        }
 | 
			
		||||
        addCrewShipSalvagedWeaponSkin(
 | 
			
		||||
            inventory,
 | 
			
		||||
            payload.ItemType,
 | 
			
		||||
            JSON.stringify(upgradeFingerprint),
 | 
			
		||||
            inventoryChanges
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        const meta = ExportRailjackWeapons[payload.ItemType];
 | 
			
		||||
        let defaultOverwrites: Partial<IEquipmentDatabase> | undefined;
 | 
			
		||||
        if (meta.defaultUpgrades?.[0]) {
 | 
			
		||||
            const upgradeType = meta.defaultUpgrades[0].ItemType;
 | 
			
		||||
            const upgradeMeta = ExportUpgrades[upgradeType];
 | 
			
		||||
            const buffs: IFingerprintStat[] = [];
 | 
			
		||||
            for (const buff of upgradeMeta.upgradeEntries!) {
 | 
			
		||||
                buffs.push({
 | 
			
		||||
                    Tag: buff.tag,
 | 
			
		||||
                    Value: Math.trunc(Math.random() * 0x40000000)
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            defaultOverwrites = {
 | 
			
		||||
                UpgradeType: upgradeType,
 | 
			
		||||
                UpgradeFingerprint: JSON.stringify({
 | 
			
		||||
                    compat: payload.ItemType,
 | 
			
		||||
                    buffs
 | 
			
		||||
                } satisfies IInnateDamageFingerprint)
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inventoryChanges.CrewShipRawSalvage = [
 | 
			
		||||
        {
 | 
			
		||||
            ItemType: payload.ItemType,
 | 
			
		||||
            ItemCount: -1
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
    addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICrewShipIdentifySalvageRequest {
 | 
			
		||||
    ItemType: string;
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +1,12 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { hasAccessToDojo, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountForRequest, getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body));
 | 
			
		||||
    const guild = (await Guild.findById(data.g, "DojoComponents Ranks"))!;
 | 
			
		||||
    const guild = (await Guild.findById(data.g, "DojoComponents"))!;
 | 
			
		||||
    const component = guild.DojoComponents.id(data.c)!;
 | 
			
		||||
    if (req.query.act == "f") {
 | 
			
		||||
        res.json({
 | 
			
		||||
@ -37,19 +34,6 @@ export const customObstacleCourseLeaderboardController: RequestHandler = async (
 | 
			
		||||
            entry.r = ++r;
 | 
			
		||||
        }
 | 
			
		||||
        await guild.save();
 | 
			
		||||
        res.status(200).end();
 | 
			
		||||
    } else if (req.query.act == "c") {
 | 
			
		||||
        // TOVERIFY: What clan permission is actually needed for this?
 | 
			
		||||
        const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
        const inventory = await getInventory(accountId, "GuildId LevelKeys");
 | 
			
		||||
        if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
 | 
			
		||||
            res.status(400).end();
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        component.Leaderboard = undefined;
 | 
			
		||||
        await guild.save();
 | 
			
		||||
 | 
			
		||||
        res.status(200).end();
 | 
			
		||||
    } else {
 | 
			
		||||
        logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
import { getGuildForRequest, hasGuildPermission } from "../../services/guildService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IGuildRank } from "../../types/guildTypes.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission, IGuildRank } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const customizeGuildRanksController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { AllianceMember, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const declineAllianceInviteController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Check requester is a warlord in their guild
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const declineGuildInviteController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,9 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { deleteSession } from "../../managers/sessionManager.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { version_compare } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { deleteSession } from "@/src/managers/sessionManager";
 | 
			
		||||
 | 
			
		||||
const deleteSessionController: RequestHandler = async (_req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(_req);
 | 
			
		||||
const deleteSessionController: RequestHandler = (_req, res) => {
 | 
			
		||||
    deleteSession(_req.query.sessionId as string);
 | 
			
		||||
    if (account.BuildLabel && version_compare(account.BuildLabel, "2016.07.08.16.56") < 0) {
 | 
			
		||||
        // Pre-Specters of the Rail
 | 
			
		||||
        res.send(_req.query.sessionId as string); // Unsure if this is correct, but the client is chill with it
 | 
			
		||||
    } else {
 | 
			
		||||
    res.sendStatus(200);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { deleteSessionController };
 | 
			
		||||
 | 
			
		||||
@ -3,14 +3,12 @@ import {
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    hasGuildPermission,
 | 
			
		||||
    refundDojoDeco,
 | 
			
		||||
    removeDojoDeco
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const destroyDojoDecoController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -20,20 +18,9 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
 | 
			
		||||
        res.json({ DojoRequestStatus: -1 });
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
 | 
			
		||||
    if ("DecoType" in request) {
 | 
			
		||||
    const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
 | 
			
		||||
 | 
			
		||||
    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();
 | 
			
		||||
    res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
			
		||||
@ -44,8 +31,3 @@ interface IDestroyDojoDecoRequest {
 | 
			
		||||
    ComponentId: string;
 | 
			
		||||
    DecoId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IClearObstacleCourseRequest {
 | 
			
		||||
    ComponentId: string;
 | 
			
		||||
    Act: "cObst" | "maybesomethingelsewedontknowabout";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { GuildPermission } from "../../types/guildTypes.ts";
 | 
			
		||||
import { parallelForeach } from "../../utils/async-utils.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
			
		||||
import { parallelForeach } from "@/src/utils/async-utils";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const divvyAllianceVaultController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits.
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,10 @@
 | 
			
		||||
import type { TGuildDatabaseDocument } from "../../models/guildModel.ts";
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import {
 | 
			
		||||
    getDojoClient,
 | 
			
		||||
    getGuildForRequestEx,
 | 
			
		||||
    hasAccessToDojo,
 | 
			
		||||
    scaleRequiredCount
 | 
			
		||||
} from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory, updateCurrency } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IDojoContributable } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import type { IDojoBuild } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
 | 
			
		||||
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IDojoContributable } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportDojoRecipes, IDojoBuild } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
interface IDojoComponentRushRequest {
 | 
			
		||||
    DecoType?: string;
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,5 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL.
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const dojoController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.json("-1"); // Tell client to use authorised request.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setDojoURLController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,13 @@
 | 
			
		||||
import { toMongoDate, toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { fromStoreItem } from "../../services/itemDataService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getRandomInt, getRandomWeightedRewardUc } from "../../services/rngService.ts";
 | 
			
		||||
import type { IMongoDate, IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { IDroneClient } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { fromStoreItem } from "@/src/services/itemDataService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getRandomInt, getRandomWeightedRewardUc } from "@/src/services/rngService";
 | 
			
		||||
import { IMongoDate, IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { IDroneClient } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportDrones, ExportResources, ExportSystems } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const dronesController: RequestHandler = async (req, res) => {
 | 
			
		||||
@ -38,13 +39,10 @@ export const dronesController: RequestHandler = async (req, res) => {
 | 
			
		||||
            ActiveDrones: activeDrones
 | 
			
		||||
        });
 | 
			
		||||
    } else if ("droneId" in req.query && "systemIndex" in req.query) {
 | 
			
		||||
        const inventory = await getInventory(
 | 
			
		||||
            accountId,
 | 
			
		||||
            "Drones instantResourceExtractorDrones noResourceExtractorDronesDamage"
 | 
			
		||||
        );
 | 
			
		||||
        const inventory = await getInventory(accountId, "Drones");
 | 
			
		||||
        const drone = inventory.Drones.id(req.query.droneId as string)!;
 | 
			
		||||
        const droneMeta = ExportDrones[drone.ItemType];
 | 
			
		||||
        drone.DeployTime = inventory.instantResourceExtractorDrones ? new Date(0) : new Date();
 | 
			
		||||
        drone.DeployTime = config.instantResourceExtractorDrones ? new Date(0) : new Date();
 | 
			
		||||
        if (drone.RepairStart) {
 | 
			
		||||
            const repairMinutes = (Date.now() - drone.RepairStart.getTime()) / 60_000;
 | 
			
		||||
            const hpPerMinute = droneMeta.repairRate / 60;
 | 
			
		||||
@ -53,11 +51,11 @@ export const dronesController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }
 | 
			
		||||
        drone.System = parseInt(req.query.systemIndex as string);
 | 
			
		||||
        const system = ExportSystems[drone.System - 1];
 | 
			
		||||
        drone.DamageTime = inventory.instantResourceExtractorDrones
 | 
			
		||||
        drone.DamageTime = config.instantResourceExtractorDrones
 | 
			
		||||
            ? new Date()
 | 
			
		||||
            : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
 | 
			
		||||
        drone.PendingDamage =
 | 
			
		||||
            !inventory.noResourceExtractorDronesDamage && Math.random() < system.damageChance
 | 
			
		||||
            Math.random() < system.damageChance
 | 
			
		||||
                ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
 | 
			
		||||
                : 0;
 | 
			
		||||
        const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;
 | 
			
		||||
@ -74,7 +72,7 @@ export const dronesController: RequestHandler = async (req, res) => {
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            drone.ResourceCount = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
 | 
			
		||||
            drone.ResourceCount = 1;
 | 
			
		||||
        }
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json({});
 | 
			
		||||
 | 
			
		||||
@ -1,534 +1,60 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { combineInventoryChanges, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type {
 | 
			
		||||
    IEndlessXpReward,
 | 
			
		||||
    IInventoryClient,
 | 
			
		||||
    TEndlessXpCategory
 | 
			
		||||
} from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { ICountedStoreItem } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportRewards } from "warframe-public-export-plus";
 | 
			
		||||
import { getRandomElement } from "../../services/rngService.ts";
 | 
			
		||||
import { handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
 | 
			
		||||
export const endlessXpController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
 | 
			
		||||
    if (payload.Mode == "r") {
 | 
			
		||||
        const inventory = await getInventory(accountId, "EndlessXP");
 | 
			
		||||
 | 
			
		||||
    inventory.EndlessXP ??= [];
 | 
			
		||||
        let entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
 | 
			
		||||
        if (!entry) {
 | 
			
		||||
            entry = {
 | 
			
		||||
    const entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
 | 
			
		||||
    if (entry) {
 | 
			
		||||
        entry.Choices = payload.Choices;
 | 
			
		||||
    } else {
 | 
			
		||||
        inventory.EndlessXP.push({
 | 
			
		||||
            Category: payload.Category,
 | 
			
		||||
            Choices: payload.Choices
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.json({
 | 
			
		||||
        NewProgress: {
 | 
			
		||||
            Category: payload.Category,
 | 
			
		||||
            Earn: 0,
 | 
			
		||||
            Claim: 0,
 | 
			
		||||
            BonusAvailable: {
 | 
			
		||||
                $date: {
 | 
			
		||||
                    $numberLong: "9999999999999"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            Expiry: {
 | 
			
		||||
                $date: {
 | 
			
		||||
                    $numberLong: "9999999999999"
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            Choices: payload.Choices,
 | 
			
		||||
                PendingRewards: []
 | 
			
		||||
            };
 | 
			
		||||
            inventory.EndlessXP.push(entry);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000;
 | 
			
		||||
        const weekEnd = weekStart + 604800000;
 | 
			
		||||
 | 
			
		||||
        entry.Earn = 0;
 | 
			
		||||
        entry.Claim = 0;
 | 
			
		||||
        entry.BonusAvailable = new Date(weekStart);
 | 
			
		||||
        entry.Expiry = new Date(weekEnd);
 | 
			
		||||
        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 [
 | 
			
		||||
            PendingRewards: [
 | 
			
		||||
                {
 | 
			
		||||
                    RequiredTotalXp: 190,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 400,
 | 
			
		||||
                    Rewards: [
 | 
			
		||||
                        {
 | 
			
		||||
                    StoreItem: choiceRewards[0],
 | 
			
		||||
                    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],
 | 
			
		||||
                            StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod",
 | 
			
		||||
                            ItemCount: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
    ];
 | 
			
		||||
                // ...
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const hardModeChosenRewards: Record<string, string> = {
 | 
			
		||||
    Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker",
 | 
			
		||||
    Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker",
 | 
			
		||||
    Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker",
 | 
			
		||||
    Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker",
 | 
			
		||||
    Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
 | 
			
		||||
    Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
 | 
			
		||||
    Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
 | 
			
		||||
    Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
 | 
			
		||||
    Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
 | 
			
		||||
    Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
 | 
			
		||||
    Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
 | 
			
		||||
    Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
 | 
			
		||||
    Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
 | 
			
		||||
    Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
 | 
			
		||||
    Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
 | 
			
		||||
    Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
 | 
			
		||||
    Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
 | 
			
		||||
    Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
 | 
			
		||||
    Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
 | 
			
		||||
    CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
 | 
			
		||||
    Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
 | 
			
		||||
    DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
 | 
			
		||||
    DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
 | 
			
		||||
    Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
 | 
			
		||||
    Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
 | 
			
		||||
    AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
 | 
			
		||||
    Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
 | 
			
		||||
    Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
 | 
			
		||||
    NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
 | 
			
		||||
    Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
 | 
			
		||||
    Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
 | 
			
		||||
    Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
 | 
			
		||||
    Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
 | 
			
		||||
    Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
 | 
			
		||||
    Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
 | 
			
		||||
    Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
 | 
			
		||||
    Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
 | 
			
		||||
    Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
 | 
			
		||||
    Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
 | 
			
		||||
    Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
 | 
			
		||||
    RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
 | 
			
		||||
    RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
 | 
			
		||||
    RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
 | 
			
		||||
    Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
 | 
			
		||||
    return [
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 285,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 600,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 945,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 1335,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 1785,
 | 
			
		||||
            Rewards: [
 | 
			
		||||
                {
 | 
			
		||||
                    StoreItem: hardModeChosenRewards[choices[0]],
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 2310,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 2925,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 3645,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 4485,
 | 
			
		||||
            Rewards: generateRandomRewards(
 | 
			
		||||
                "/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
 | 
			
		||||
            )
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            RequiredTotalXp: 5460,
 | 
			
		||||
            Rewards: [
 | 
			
		||||
                {
 | 
			
		||||
                    StoreItem: hardModeChosenRewards[choices[1]],
 | 
			
		||||
                    ItemCount: 1
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
};
 | 
			
		||||
interface IEndlessXpRequest {
 | 
			
		||||
    Mode: string; // "r"
 | 
			
		||||
    Category: TEndlessXpCategory;
 | 
			
		||||
    Choices: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory, updateEntratiVault } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const entratiLabConquestModeController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -11,7 +11,24 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
 | 
			
		||||
        "EntratiVaultCountResetDate EntratiVaultCountLastPeriod EntratiLabConquestUnlocked EchoesHexConquestUnlocked EchoesHexConquestActiveFrameVariants EchoesHexConquestActiveStickers EntratiLabConquestActiveFrameVariants EntratiLabConquestCacheScoreMission EchoesHexConquestCacheScoreMission"
 | 
			
		||||
    );
 | 
			
		||||
    const body = getJSONfromString<IEntratiLabConquestModeRequest>(String(req.body));
 | 
			
		||||
    updateEntratiVault(inventory);
 | 
			
		||||
    if (!inventory.EntratiVaultCountResetDate || Date.now() >= inventory.EntratiVaultCountResetDate.getTime()) {
 | 
			
		||||
        const EPOCH = 1734307200 * 1000; // Mondays, amirite?
 | 
			
		||||
        const day = Math.trunc((Date.now() - EPOCH) / 86400000);
 | 
			
		||||
        const week = Math.trunc(day / 7);
 | 
			
		||||
        const weekStart = EPOCH + week * 604800000;
 | 
			
		||||
        const weekEnd = weekStart + 604800000;
 | 
			
		||||
        inventory.EntratiVaultCountLastPeriod = 0;
 | 
			
		||||
        inventory.EntratiVaultCountResetDate = new Date(weekEnd);
 | 
			
		||||
        if (inventory.EntratiLabConquestUnlocked) {
 | 
			
		||||
            inventory.EntratiLabConquestUnlocked = 0;
 | 
			
		||||
            inventory.EntratiLabConquestActiveFrameVariants = [];
 | 
			
		||||
        }
 | 
			
		||||
        if (inventory.EchoesHexConquestUnlocked) {
 | 
			
		||||
            inventory.EchoesHexConquestUnlocked = 0;
 | 
			
		||||
            inventory.EchoesHexConquestActiveFrameVariants = [];
 | 
			
		||||
            inventory.EchoesHexConquestActiveStickers = [];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    if (body.BuyMode) {
 | 
			
		||||
        inventory.EntratiVaultCountLastPeriod! += 2;
 | 
			
		||||
        if (body.IsEchoesDeepArchemedea) {
 | 
			
		||||
@ -32,7 +49,7 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate!),
 | 
			
		||||
        EntratiVaultCountResetDate: toMongoDate(inventory.EntratiVaultCountResetDate),
 | 
			
		||||
        EntratiVaultCountLastPeriod: inventory.EntratiVaultCountLastPeriod,
 | 
			
		||||
        EntratiLabConquestUnlocked: inventory.EntratiLabConquestUnlocked,
 | 
			
		||||
        EntratiLabConquestCacheScoreMission: inventory.EntratiLabConquestCacheScoreMission,
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,9 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { WeaponTypeInternal } from "../../services/itemDataService.ts";
 | 
			
		||||
import { getRecipe } from "../../services/itemDataService.ts";
 | 
			
		||||
import { EquipmentFeatures } from "../../types/equipmentTypes.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getRecipe, WeaponTypeInternal } from "@/src/services/itemDataService";
 | 
			
		||||
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
 | 
			
		||||
export const evolveWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const feedPrinceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "MiscItems NokkoColony NodeIntrosCompleted");
 | 
			
		||||
    const payload = getJSONfromString<IFeedPrinceRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    switch (payload.Mode) {
 | 
			
		||||
        case "r": {
 | 
			
		||||
            inventory.NokkoColony ??= {
 | 
			
		||||
                FeedLevel: 0,
 | 
			
		||||
                JournalEntries: []
 | 
			
		||||
            };
 | 
			
		||||
            const InventoryChanges: IInventoryChanges = {};
 | 
			
		||||
            inventory.NokkoColony.FeedLevel += payload.Amount;
 | 
			
		||||
            if (
 | 
			
		||||
                (!inventory.NodeIntrosCompleted.includes("CompletedVision1") && inventory.NokkoColony.FeedLevel > 20) ||
 | 
			
		||||
                (!inventory.NodeIntrosCompleted.includes("CompletedVision2") && inventory.NokkoColony.FeedLevel > 60)
 | 
			
		||||
            ) {
 | 
			
		||||
                res.json({
 | 
			
		||||
                    FeedSucceeded: false,
 | 
			
		||||
                    FeedLevel: inventory.NokkoColony.FeedLevel - payload.Amount,
 | 
			
		||||
                    InventoryChanges
 | 
			
		||||
                } satisfies IFeedPrinceResponse);
 | 
			
		||||
            } else {
 | 
			
		||||
                addMiscItem(
 | 
			
		||||
                    inventory,
 | 
			
		||||
                    "/Lotus/Types/Items/MiscItems/MushroomFood",
 | 
			
		||||
                    payload.Amount * -1,
 | 
			
		||||
                    InventoryChanges
 | 
			
		||||
                );
 | 
			
		||||
                await inventory.save();
 | 
			
		||||
                res.json({
 | 
			
		||||
                    FeedSucceeded: true,
 | 
			
		||||
                    FeedLevel: inventory.NokkoColony.FeedLevel,
 | 
			
		||||
                    InventoryChanges
 | 
			
		||||
                } satisfies IFeedPrinceResponse);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
            logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
 | 
			
		||||
            throw new Error(`unknown feedPrince mode: ${payload.Mode}`);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IFeedPrinceRequest {
 | 
			
		||||
    Mode: string; // r
 | 
			
		||||
    Amount: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IFeedPrinceResponse {
 | 
			
		||||
    FeedSucceeded: boolean;
 | 
			
		||||
    FeedLevel: number;
 | 
			
		||||
    InventoryChanges: IInventoryChanges;
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getSession } from "../../managers/sessionManager.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import type { IFindSessionRequest } from "../../types/session.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getSession } from "@/src/managers/sessionManager";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { IFindSessionRequest } from "@/src/types/session";
 | 
			
		||||
 | 
			
		||||
export const findSessionsController: RequestHandler = (_req, res) => {
 | 
			
		||||
    const req = JSON.parse(String(_req.body)) as IFindSessionRequest;
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { addMiscItems, addStanding, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
 | 
			
		||||
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const fishmongerController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -30,7 +31,25 @@ export const fishmongerController: RequestHandler = async (req, res) => {
 | 
			
		||||
        miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
 | 
			
		||||
    }
 | 
			
		||||
    addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
    if (gainedStanding && syndicateTag) addStanding(inventory, syndicateTag, gainedStanding);
 | 
			
		||||
    if (gainedStanding && syndicateTag) {
 | 
			
		||||
        let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
 | 
			
		||||
        if (!syndicate) {
 | 
			
		||||
            syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
 | 
			
		||||
        }
 | 
			
		||||
        const syndicateMeta = ExportSyndicates[syndicateTag];
 | 
			
		||||
 | 
			
		||||
        const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
 | 
			
		||||
        if (syndicate.Standing + gainedStanding > max) {
 | 
			
		||||
            gainedStanding = max - syndicate.Standing;
 | 
			
		||||
        }
 | 
			
		||||
        if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
 | 
			
		||||
            gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        syndicate.Standing += gainedStanding;
 | 
			
		||||
 | 
			
		||||
        updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: {
 | 
			
		||||
 | 
			
		||||
@ -1,91 +1,23 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory, addMiscItems, addEquipment, occupySlot } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IMiscItem, TFocusPolarity, TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/services/inventoryService";
 | 
			
		||||
import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { ExportFocusUpgrades } from "warframe-public-export-plus";
 | 
			
		||||
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { version_compare } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
 | 
			
		||||
export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
 | 
			
		||||
    let op = req.query.op as string;
 | 
			
		||||
    const focus2 = account.BuildLabel && version_compare(account.BuildLabel, "2022.04.29.12.53") < 0;
 | 
			
		||||
    if (focus2) {
 | 
			
		||||
        // Focus 2.0
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    switch (req.query.op) {
 | 
			
		||||
            case Focus2Operation.InstallLens:
 | 
			
		||||
                op = "InstallLens";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.UnlockWay:
 | 
			
		||||
                op = "UnlockWay";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.UnlockUpgrade:
 | 
			
		||||
                op = "UnlockUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.IncreasePool:
 | 
			
		||||
                op = "IncreasePool";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.LevelUpUpgrade:
 | 
			
		||||
                op = "LevelUpUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.ActivateWay:
 | 
			
		||||
                op = "ActivateWay";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.UpdateUpgrade:
 | 
			
		||||
                op = "UpdateUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.SentTrainingAmplifier:
 | 
			
		||||
                op = "SentTrainingAmplifier";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.UnbindUpgrade:
 | 
			
		||||
                op = "UnbindUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus2Operation.ConvertShard:
 | 
			
		||||
                op = "ConvertShard";
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        // Focus 3.0
 | 
			
		||||
        switch (req.query.op) {
 | 
			
		||||
            case Focus3Operation.InstallLens:
 | 
			
		||||
                op = "InstallLens";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.UnlockWay:
 | 
			
		||||
                op = "UnlockWay";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.UnlockUpgrade:
 | 
			
		||||
                op = "UnlockUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.LevelUpUpgrade:
 | 
			
		||||
                op = "LevelUpUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.ActivateWay:
 | 
			
		||||
                op = "ActivateWay";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.SentTrainingAmplifier:
 | 
			
		||||
                op = "SentTrainingAmplifier";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.UnbindUpgrade:
 | 
			
		||||
                op = "UnbindUpgrade";
 | 
			
		||||
                break;
 | 
			
		||||
            case Focus3Operation.ConvertShard:
 | 
			
		||||
                op = "ConvertShard";
 | 
			
		||||
                break;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch (op) {
 | 
			
		||||
        default:
 | 
			
		||||
            logger.error("Unhandled focus op type: " + String(req.query.op));
 | 
			
		||||
            logger.debug(String(req.body));
 | 
			
		||||
            res.end();
 | 
			
		||||
            break;
 | 
			
		||||
        case "InstallLens": {
 | 
			
		||||
        case FocusOperation.InstallLens: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as ILensInstallRequest;
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            const item = inventory[request.Category].id(request.WeaponId);
 | 
			
		||||
            if (item) {
 | 
			
		||||
                item.FocusLens = request.LensType;
 | 
			
		||||
@ -103,15 +35,15 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "UnlockWay": {
 | 
			
		||||
        case FocusOperation.UnlockWay: {
 | 
			
		||||
            const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
 | 
			
		||||
            const focusPolarity = focusTypeToPolarity(focusType);
 | 
			
		||||
            const inventory = await getInventory(account._id.toString(), "FocusAbility FocusUpgrades FocusXP");
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            const cost = inventory.FocusAbility ? 50_000 : 0;
 | 
			
		||||
            inventory.FocusAbility ??= focusType;
 | 
			
		||||
            inventory.FocusUpgrades.push({ ItemType: focusType });
 | 
			
		||||
            if (cost) {
 | 
			
		||||
                inventory.FocusXP![focusPolarity]! -= cost;
 | 
			
		||||
            if (inventory.FocusXP) {
 | 
			
		||||
                inventory.FocusXP[focusPolarity] -= cost;
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
@ -120,57 +52,31 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "IncreasePool": {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as IIncreasePoolRequest;
 | 
			
		||||
            const focusPolarity = focusTypeToPolarity(request.FocusType);
 | 
			
		||||
            const inventory = await getInventory(account._id.toString(), "FocusXP FocusCapacity");
 | 
			
		||||
            let cost = 0;
 | 
			
		||||
            for (let capacity = request.CurrentTotalCapacity; capacity != request.NewTotalCapacity; ++capacity) {
 | 
			
		||||
                cost += increasePoolCost[capacity - 5];
 | 
			
		||||
            }
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= cost;
 | 
			
		||||
            inventory.FocusCapacity = request.NewTotalCapacity;
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                TotalCapacity: request.NewTotalCapacity,
 | 
			
		||||
                FocusPointCosts: { [focusPolarity]: cost }
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "ActivateWay": {
 | 
			
		||||
        case FocusOperation.ActivateWay: {
 | 
			
		||||
            const focusType = (JSON.parse(String(req.body)) as IWayRequest).FocusType;
 | 
			
		||||
 | 
			
		||||
            await Inventory.updateOne(
 | 
			
		||||
                {
 | 
			
		||||
                    accountOwnerId: account._id.toString()
 | 
			
		||||
                    accountOwnerId: accountId
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    FocusAbility: focusType
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            res.json({
 | 
			
		||||
                FocusUpgrade: { ItemType: focusType }
 | 
			
		||||
            });
 | 
			
		||||
            res.end();
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "UnlockUpgrade": {
 | 
			
		||||
        case FocusOperation.UnlockUpgrade: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as IUnlockUpgradeRequest;
 | 
			
		||||
            const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            let cost = 0;
 | 
			
		||||
            for (const focusType of request.FocusTypes) {
 | 
			
		||||
                if (focusType in ExportFocusUpgrades) {
 | 
			
		||||
                cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
 | 
			
		||||
                } else if (focusType == "/Lotus/Upgrades/Focus/Power/Residual/ChannelEfficiencyFocusUpgrade") {
 | 
			
		||||
                    // Zenurik's Inner Might (Focus 2.0)
 | 
			
		||||
                    cost += 50_000;
 | 
			
		||||
                } else {
 | 
			
		||||
                    logger.warn(`unknown focus upgrade ${focusType}, will unlock it for free`);
 | 
			
		||||
                }
 | 
			
		||||
                inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
 | 
			
		||||
            }
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= cost;
 | 
			
		||||
            inventory.FocusXP![focusPolarity] -= cost;
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                FocusTypes: request.FocusTypes,
 | 
			
		||||
@ -178,22 +84,17 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "LevelUpUpgrade":
 | 
			
		||||
        case "UpdateUpgrade": {
 | 
			
		||||
        case FocusOperation.LevelUpUpgrade: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as ILevelUpUpgradeRequest;
 | 
			
		||||
            const focusPolarity = focusTypeToPolarity(request.FocusInfos[0].ItemType);
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            let cost = 0;
 | 
			
		||||
            for (const focusUpgrade of request.FocusInfos) {
 | 
			
		||||
                cost += focusUpgrade.FocusXpCost;
 | 
			
		||||
                const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
 | 
			
		||||
                if (op == "UpdateUpgrade") {
 | 
			
		||||
                    focusUpgradeDb.IsActive = focusUpgrade.IsActive;
 | 
			
		||||
                } else {
 | 
			
		||||
                focusUpgradeDb.Level = focusUpgrade.Level;
 | 
			
		||||
            }
 | 
			
		||||
            }
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= cost;
 | 
			
		||||
            inventory.FocusXP![focusPolarity] -= cost;
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({
 | 
			
		||||
                FocusInfos: request.FocusInfos,
 | 
			
		||||
@ -201,26 +102,25 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "SentTrainingAmplifier": {
 | 
			
		||||
        case FocusOperation.SentTrainingAmplifier: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
 | 
			
		||||
                ModularParts: [
 | 
			
		||||
            const parts: string[] = [
 | 
			
		||||
                "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
 | 
			
		||||
                "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
 | 
			
		||||
                "/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
 | 
			
		||||
                ]
 | 
			
		||||
            });
 | 
			
		||||
            ];
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
 | 
			
		||||
            occupySlot(inventory, InventorySlot.AMPS, false);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json(inventoryChanges.OperatorAmps![0]);
 | 
			
		||||
            res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "UnbindUpgrade": {
 | 
			
		||||
        case FocusOperation.UnbindUpgrade: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
 | 
			
		||||
            const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length;
 | 
			
		||||
            addMiscItems(inventory, [
 | 
			
		||||
                {
 | 
			
		||||
                    ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
 | 
			
		||||
@ -246,7 +146,7 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            });
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        case "ConvertShard": {
 | 
			
		||||
        case FocusOperation.ConvertShard: {
 | 
			
		||||
            const request = JSON.parse(String(req.body)) as IConvertShardRequest;
 | 
			
		||||
            // Tally XP
 | 
			
		||||
            let xp = 0;
 | 
			
		||||
@ -264,11 +164,9 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
            for (const shard of request.Shards) {
 | 
			
		||||
                shard.ItemCount *= -1;
 | 
			
		||||
            }
 | 
			
		||||
            const inventory = await getInventory(account._id.toString());
 | 
			
		||||
            const polarity = request.Polarity;
 | 
			
		||||
            inventory.FocusXP ??= {};
 | 
			
		||||
            inventory.FocusXP[polarity] ??= 0;
 | 
			
		||||
            inventory.FocusXP[polarity] += xp;
 | 
			
		||||
            const inventory = await getInventory(accountId);
 | 
			
		||||
            inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 };
 | 
			
		||||
            inventory.FocusXP[request.Polarity] += xp;
 | 
			
		||||
            addMiscItems(inventory, request.Shards);
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            break;
 | 
			
		||||
@ -276,8 +174,7 @@ export const focusController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Focus 3.0
 | 
			
		||||
enum Focus3Operation {
 | 
			
		||||
enum FocusOperation {
 | 
			
		||||
    InstallLens = "1",
 | 
			
		||||
    UnlockWay = "2",
 | 
			
		||||
    UnlockUpgrade = "3",
 | 
			
		||||
@ -288,20 +185,6 @@ enum Focus3Operation {
 | 
			
		||||
    ConvertShard = "9"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Focus 2.0
 | 
			
		||||
enum Focus2Operation {
 | 
			
		||||
    InstallLens = "1",
 | 
			
		||||
    UnlockWay = "2",
 | 
			
		||||
    UnlockUpgrade = "3",
 | 
			
		||||
    IncreasePool = "4",
 | 
			
		||||
    LevelUpUpgrade = "5",
 | 
			
		||||
    ActivateWay = "6",
 | 
			
		||||
    UpdateUpgrade = "7", // used to change the IsActive state, same format as ILevelUpUpgradeRequest
 | 
			
		||||
    SentTrainingAmplifier = "9",
 | 
			
		||||
    UnbindUpgrade = "10",
 | 
			
		||||
    ConvertShard = "11"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// For UnlockWay & ActivateWay
 | 
			
		||||
interface IWayRequest {
 | 
			
		||||
    FocusType: string;
 | 
			
		||||
@ -311,13 +194,6 @@ interface IUnlockUpgradeRequest {
 | 
			
		||||
    FocusTypes: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Focus 2.0
 | 
			
		||||
interface IIncreasePoolRequest {
 | 
			
		||||
    FocusType: string;
 | 
			
		||||
    CurrentTotalCapacity: number;
 | 
			
		||||
    NewTotalCapacity: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ILevelUpUpgradeRequest {
 | 
			
		||||
    FocusInfos: {
 | 
			
		||||
        ItemType: string;
 | 
			
		||||
@ -325,7 +201,6 @@ interface ILevelUpUpgradeRequest {
 | 
			
		||||
        IsUniversal: boolean;
 | 
			
		||||
        Level: number;
 | 
			
		||||
        IsActiveAbility: boolean;
 | 
			
		||||
        IsActive?: number; // Focus 2.0
 | 
			
		||||
    }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -351,7 +226,7 @@ interface ILensInstallRequest {
 | 
			
		||||
 | 
			
		||||
// Works for ways & upgrades
 | 
			
		||||
const focusTypeToPolarity = (type: string): TFocusPolarity => {
 | 
			
		||||
    return ("AP_" + type.substring(1).split("/")[3].toUpperCase()) as TFocusPolarity;
 | 
			
		||||
    return ("AP_" + type.substr(1).split("/")[3].toUpperCase()) as TFocusPolarity;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const shardValues = {
 | 
			
		||||
@ -360,19 +235,3 @@ const shardValues = {
 | 
			
		||||
    "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem": 25_000,
 | 
			
		||||
    "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantTierTwoItem": 40_000
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Starting at a capacity of 5 (Source: https://wiki.warframe.com/w/Focus_2.0)
 | 
			
		||||
const increasePoolCost = [
 | 
			
		||||
    2576, 3099, 3638, 4190, 4755, 5331, 5918, 6514, 7120, 7734, 8357, 8988, 9626, 10271, 10923, 11582, 12247, 12918,
 | 
			
		||||
    13595, 14277, 14965, 15659, 16357, 17061, 17769, 18482, 19200, 19922, 20649, 21380, 22115, 22854, 23597, 24344,
 | 
			
		||||
    25095, 25850, 26609, 27371, 28136, 28905, 29678, 30454, 31233, 32015, 32801, 33590, 34382, 35176, 35974, 36775,
 | 
			
		||||
    37579, 38386, 39195, 40008, 40823, 41641, 42461, 43284, 44110, 44938, 45769, 46603, 47439, 48277, 49118, 49961,
 | 
			
		||||
    50807, 51655, 52505, 53357, 54212, 55069, 55929, 56790, 57654, 58520, 59388, 60258, 61130, 62005, 62881, 63759,
 | 
			
		||||
    64640, 65522, 66407, 67293, 68182, 69072, 69964, 70858, 71754, 72652, 73552, 74453, 75357, 76262, 77169, 78078,
 | 
			
		||||
    78988, 79900, 80814, 81730, 82648, 83567, 84488, 85410, 86334, 87260, 88188, 89117, 90047, 90980, 91914, 92849,
 | 
			
		||||
    93786, 94725, 95665, 96607, 97550, 98495, 99441, 100389, 101338, 102289, 103241, 104195, 105150, 106107, 107065,
 | 
			
		||||
    108024, 108985, 109948, 110911, 111877, 112843, 113811, 114780, 115751, 116723, 117696, 118671, 119647, 120624,
 | 
			
		||||
    121603, 122583, 123564, 124547, 125531, 126516, 127503, 128490, 129479, 130470, 131461, 132454, 133448, 134443,
 | 
			
		||||
    135440, 136438, 137437, 138437, 139438, 140441, 141444, 142449, 143455, 144463, 145471, 146481, 147492, 148503,
 | 
			
		||||
    149517
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -1,27 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
 | 
			
		||||
export const forceRemoveItemController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "MiscItems");
 | 
			
		||||
    const body = getJSONfromString<IForceRemoveItemRequest>(String(req.body));
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    for (const item of body.items) {
 | 
			
		||||
        const index = inventory.MiscItems.findIndex(x => x.ItemType == item);
 | 
			
		||||
        if (index != -1) {
 | 
			
		||||
            inventoryChanges.MiscItems ??= [];
 | 
			
		||||
            inventoryChanges.MiscItems.push({ ItemType: item, ItemCount: inventory.MiscItems[index].ItemCount * -1 });
 | 
			
		||||
 | 
			
		||||
            inventory.MiscItems.splice(index, 1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({ InventoryChanges: inventoryChanges });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IForceRemoveItemRequest {
 | 
			
		||||
    items: string[];
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +1,23 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { addFusionTreasures, addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { IMiscItem } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
 | 
			
		||||
interface IFusionTreasureRequest {
 | 
			
		||||
    oldTreasureName: string;
 | 
			
		||||
    newTreasureName: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const parseFusionTreasure = (name: string, count: number): IFusionTreasure => {
 | 
			
		||||
    const arr = name.split("_");
 | 
			
		||||
    return {
 | 
			
		||||
        ItemType: arr[0],
 | 
			
		||||
        Sockets: parseInt(arr[1], 16),
 | 
			
		||||
        ItemCount: count
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fusionTreasuresController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
 | 
			
		||||
@ -1,83 +0,0 @@
 | 
			
		||||
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { addMiscItem, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { toStoreItem } from "../../services/itemDataService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
			
		||||
import type { IMongoDate } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { IMissionReward } from "../../types/missionTypes.ts";
 | 
			
		||||
import type { IGardeningClient, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { dict_en, ExportResources } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const gardeningController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const data = getJSONfromString<IGardeningRequest>(String(req.body));
 | 
			
		||||
    if (data.Mode != "HarvestAll") {
 | 
			
		||||
        throw new Error(`unexpected gardening mode: ${data.Mode}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const [inventory, personalRooms] = await Promise.all([
 | 
			
		||||
        getInventory(accountId, "MiscItems"),
 | 
			
		||||
        getPersonalRooms(accountId, "Apartment")
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    // Harvest plants
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    const rewards: Record<string, IMissionReward[][]> = {};
 | 
			
		||||
    for (const planter of personalRooms.Apartment.Gardening.Planters) {
 | 
			
		||||
        rewards[planter.Name] = [];
 | 
			
		||||
        for (const plant of planter.Plants) {
 | 
			
		||||
            const itemType =
 | 
			
		||||
                "/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" +
 | 
			
		||||
                plant.PlantType.substring(plant.PlantType.length - 1);
 | 
			
		||||
            const itemCount = Math.random() < 0.775 ? 2 : 4;
 | 
			
		||||
 | 
			
		||||
            addMiscItem(inventory, itemType, itemCount, inventoryChanges);
 | 
			
		||||
 | 
			
		||||
            rewards[planter.Name].push([
 | 
			
		||||
                {
 | 
			
		||||
                    StoreItem: toStoreItem(itemType),
 | 
			
		||||
                    TypeName: itemType,
 | 
			
		||||
                    ItemCount: itemCount,
 | 
			
		||||
                    DailyCooldown: false,
 | 
			
		||||
                    Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564,
 | 
			
		||||
                    TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`,
 | 
			
		||||
                    ProductCategory: "MiscItems"
 | 
			
		||||
                }
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Refresh garden
 | 
			
		||||
    personalRooms.Apartment.Gardening = createGarden();
 | 
			
		||||
 | 
			
		||||
    await Promise.all([inventory.save(), personalRooms.save()]);
 | 
			
		||||
 | 
			
		||||
    const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1];
 | 
			
		||||
    const plant = planter.Plants[planter.Plants.length - 1];
 | 
			
		||||
    res.json({
 | 
			
		||||
        GardenTagName: planter.Name,
 | 
			
		||||
        PlantType: plant.PlantType,
 | 
			
		||||
        PlotIndex: plant.PlotIndex,
 | 
			
		||||
        EndTime: toMongoDate(plant.EndTime),
 | 
			
		||||
        InventoryChanges: inventoryChanges,
 | 
			
		||||
        Gardening: personalRooms.toJSON<IPersonalRoomsClient>().Apartment.Gardening,
 | 
			
		||||
        Rewards: rewards
 | 
			
		||||
    } satisfies IGardeningResponse);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IGardeningRequest {
 | 
			
		||||
    Mode: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IGardeningResponse {
 | 
			
		||||
    GardenTagName: string;
 | 
			
		||||
    PlantType: string;
 | 
			
		||||
    PlotIndex: number;
 | 
			
		||||
    EndTime: IMongoDate;
 | 
			
		||||
    InventoryChanges: IInventoryChanges;
 | 
			
		||||
    Gardening: IGardeningClient;
 | 
			
		||||
    Rewards: Record<string, IMissionReward[][]>;
 | 
			
		||||
}
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { updateGeneric } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import type { IGenericUpdate } from "../../types/genericUpdate.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { updateGeneric } from "@/src/services/inventoryService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { IGenericUpdate } from "@/src/types/genericUpdate";
 | 
			
		||||
 | 
			
		||||
// This endpoint used to be /api/genericUpdate.php, but sometime around the Jade Shadows update, it was changed to /api/updateNodeIntros.php.
 | 
			
		||||
// SpaceNinjaServer supports both endpoints right now.
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { Alliance, Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAllianceClient } from "../../services/guildService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Alliance, Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { getAllianceClient } from "@/src/services/guildService";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -18,7 +18,6 @@ export const getAllianceController: RequestHandler = async (req, res) => {
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// POST request since U27
 | 
			
		||||
/*interface IGetAllianceRequest {
 | 
			
		||||
    memberCount: number;
 | 
			
		||||
    clanLeaderName: string;
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,8 @@
 | 
			
		||||
import { DailyDeal } from "../../models/worldStateModel.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getDailyDealStockLevelsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const dailyDeal = (await DailyDeal.findOne({ StoreItem: req.query.productName }, "AmountSold"))!;
 | 
			
		||||
export const getDailyDealStockLevelsController: RequestHandler = (req, res) => {
 | 
			
		||||
    res.json({
 | 
			
		||||
        StoreItem: req.query.productName,
 | 
			
		||||
        AmountSold: dailyDeal.AmountSold
 | 
			
		||||
        AmountSold: 0
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,54 +1,15 @@
 | 
			
		||||
import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { Friendship } from "../../models/friendModel.ts";
 | 
			
		||||
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
			
		||||
import type { Request, RequestHandler, Response } from "express";
 | 
			
		||||
import { Request, Response } from "express";
 | 
			
		||||
 | 
			
		||||
// POST with {} instead of GET as of 38.5.0
 | 
			
		||||
export const getFriendsController: RequestHandler = async (req: Request, res: Response) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const response: IGetFriendsResponse = {
 | 
			
		||||
        Current: [],
 | 
			
		||||
        IncomingFriendRequests: [],
 | 
			
		||||
        OutgoingFriendRequests: []
 | 
			
		||||
    };
 | 
			
		||||
    const [internalFriendships, externalFriendships] = await Promise.all([
 | 
			
		||||
        Friendship.find({ owner: accountId }),
 | 
			
		||||
        Friendship.find({ friend: accountId }, "owner Note")
 | 
			
		||||
    ]);
 | 
			
		||||
    for (const externalFriendship of externalFriendships) {
 | 
			
		||||
        if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
 | 
			
		||||
            response.IncomingFriendRequests.push({
 | 
			
		||||
                _id: toOid(externalFriendship.owner),
 | 
			
		||||
                Note: externalFriendship.Note
 | 
			
		||||
const getFriendsController = (_request: Request, response: Response): void => {
 | 
			
		||||
    response.writeHead(200, {
 | 
			
		||||
        //Connection: "keep-alive",
 | 
			
		||||
        //"Content-Encoding": "gzip",
 | 
			
		||||
        "Content-Type": "text/html",
 | 
			
		||||
        // charset: "UTF - 8",
 | 
			
		||||
        "Content-Length": "3"
 | 
			
		||||
    });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    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);
 | 
			
		||||
    response.end(Buffer.from([0x7b, 0x7d, 0x0a]));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// interface IGetFriendsResponse {
 | 
			
		||||
//     Current: IFriendInfo[];
 | 
			
		||||
//     IncomingFriendRequests: IFriendInfo[];
 | 
			
		||||
//     OutgoingFriendRequests: IFriendInfo[];
 | 
			
		||||
// }
 | 
			
		||||
type IGetFriendsResponse = Record<"Current" | "IncomingFriendRequests" | "OutgoingFriendRequests", IFriendInfo[]>;
 | 
			
		||||
export { getFriendsController };
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { GuildMember } from "../../models/guildModel.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IGuildMemberClient } from "../../types/guildTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { GuildMember } from "@/src/models/guildModel";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IGuildMemberClient } from "@/src/types/guildTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getGuildContributionsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { logger } from "../../utils/logger.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { createUniqueClanName, getGuildClient } from "../../services/guildService.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
 | 
			
		||||
 | 
			
		||||
export const getGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId");
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "GuildId");
 | 
			
		||||
    if (inventory.GuildId) {
 | 
			
		||||
        const guild = await Guild.findById(inventory.GuildId);
 | 
			
		||||
        if (guild) {
 | 
			
		||||
@ -24,7 +24,7 @@ export const getGuildController: RequestHandler = async (req, res) => {
 | 
			
		||||
                guild.CeremonyResetDate = undefined;
 | 
			
		||||
                await guild.save();
 | 
			
		||||
            }
 | 
			
		||||
            res.json(await getGuildClient(guild, account));
 | 
			
		||||
            res.json(await getGuildClient(guild, accountId));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,7 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { Types } from "mongoose";
 | 
			
		||||
import { Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { getDojoClient } from "../../services/guildService.ts";
 | 
			
		||||
import { Account } from "../../models/loginModel.ts";
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { getDojoClient } from "@/src/services/guildService";
 | 
			
		||||
 | 
			
		||||
export const getGuildDojoController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const guildId = req.query.guildId as string;
 | 
			
		||||
@ -19,15 +18,14 @@ export const getGuildDojoController: RequestHandler = async (req, res) => {
 | 
			
		||||
            _id: new Types.ObjectId(),
 | 
			
		||||
            pf: "/Lotus/Levels/ClanDojo/DojoHall.level",
 | 
			
		||||
            ppf: "",
 | 
			
		||||
            CompletionTime: new Date(Date.now() - 1000),
 | 
			
		||||
            CompletionTime: new Date(Date.now()),
 | 
			
		||||
            DecoCapacity: 600
 | 
			
		||||
        });
 | 
			
		||||
        await guild.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const payload: IGetGuildDojoRequest = req.body ? (JSON.parse(String(req.body)) as IGetGuildDojoRequest) : {};
 | 
			
		||||
    const account = await Account.findById(req.query.accountId as string);
 | 
			
		||||
    res.json(await getDojoClient(guild, 0, payload.ComponentId, account?.BuildLabel));
 | 
			
		||||
    res.json(await getDojoClient(guild, 0, payload.ComponentId));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IGetGuildDojoRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { Guild } from "../../models/guildModel.ts";
 | 
			
		||||
 | 
			
		||||
export const getGuildEventScoreController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const account = await getAccountForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(account._id.toString(), "GuildId");
 | 
			
		||||
    const guild = await Guild.findById(inventory.GuildId);
 | 
			
		||||
    const goalId = req.query.goalId as string;
 | 
			
		||||
    if (guild && guild.GoalProgress && goalId) {
 | 
			
		||||
        const goal = guild.GoalProgress.find(x => x.goalId.toString() == goalId);
 | 
			
		||||
        if (goal) {
 | 
			
		||||
            res.json({
 | 
			
		||||
                Tier: guild.Tier,
 | 
			
		||||
                GoalProgress: {
 | 
			
		||||
                    Count: goal.Count,
 | 
			
		||||
                    Tag: goal.Tag,
 | 
			
		||||
                    _id: { $oid: goal.goalId }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    res.json({});
 | 
			
		||||
};
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { toMongoDate } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { Guild } from "../../models/guildModel.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IMongoDate } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { Guild } from "@/src/models/guildModel";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IMongoDate } from "@/src/types/commonTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getGuildLogController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
@ -1,20 +1,16 @@
 | 
			
		||||
import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { Account, Ignore } from "../../models/loginModel.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IFriendInfo } from "../../types/friendTypes.ts";
 | 
			
		||||
import { parallelForeach } from "../../utils/async-utils.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getIgnoredUsersController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const ignores = await Ignore.find({ ignorer: accountId });
 | 
			
		||||
    const ignoredUsers: IFriendInfo[] = [];
 | 
			
		||||
    await parallelForeach(ignores, async ignore => {
 | 
			
		||||
        const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
 | 
			
		||||
        ignoredUsers.push({
 | 
			
		||||
            _id: toOid(ignore.ignoree),
 | 
			
		||||
            DisplayName: ignoreeAccount.DisplayName + ""
 | 
			
		||||
const getIgnoredUsersController: RequestHandler = (_req, res) => {
 | 
			
		||||
    res.writeHead(200, {
 | 
			
		||||
        "Content-Type": "text/html",
 | 
			
		||||
        "Content-Length": "3"
 | 
			
		||||
    });
 | 
			
		||||
    });
 | 
			
		||||
    res.json({ IgnoredUsers: ignoredUsers });
 | 
			
		||||
    res.end(
 | 
			
		||||
        Buffer.from([
 | 
			
		||||
            0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32,
 | 
			
		||||
            0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d
 | 
			
		||||
        ])
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { getIgnoredUsersController };
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,14 @@
 | 
			
		||||
import { Inventory } from "../../models/inventoryModels/inventoryModel.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { generateRewardSeed } from "../../services/rngService.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { generateRewardSeed } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
 | 
			
		||||
    const rewardSeed = generateRewardSeed();
 | 
			
		||||
    logger.debug(`generated new reward seed: ${rewardSeed}`);
 | 
			
		||||
    await Inventory.updateOne(
 | 
			
		||||
        {
 | 
			
		||||
            accountOwnerId: accountId
 | 
			
		||||
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getSeasonChallengePools, getWorldState, pushWeeklyActs } from "../../services/worldStateService.ts";
 | 
			
		||||
import { EPOCH, unixTimesInMs } from "../../constants/timeConstants.ts";
 | 
			
		||||
import type { ISeasonChallenge } from "../../types/worldStateTypes.ts";
 | 
			
		||||
import { ExportChallenges } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress");
 | 
			
		||||
    const worldState = getWorldState(undefined);
 | 
			
		||||
 | 
			
		||||
    if (worldState.SeasonInfo) {
 | 
			
		||||
        const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag);
 | 
			
		||||
        const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong);
 | 
			
		||||
        const nightwaveSeason = worldState.SeasonInfo.Season;
 | 
			
		||||
        const timeMs = worldState.Time * 1000;
 | 
			
		||||
        const completedChallengesIds = new Set<string>();
 | 
			
		||||
 | 
			
		||||
        inventory.SeasonChallengeHistory.forEach(challengeHistory => {
 | 
			
		||||
            const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1;
 | 
			
		||||
            if (nightwaveSeason == entryNightwaveSeason) {
 | 
			
		||||
                const meta = Object.entries(ExportChallenges).find(
 | 
			
		||||
                    ([key]) => key.split("/").pop() === challengeHistory.challenge
 | 
			
		||||
                );
 | 
			
		||||
                if (meta) {
 | 
			
		||||
                    const [, challengeMeta] = meta;
 | 
			
		||||
                    const challengeProgress = inventory.ChallengeProgress.find(
 | 
			
		||||
                        c => c.Name === challengeHistory.challenge
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) {
 | 
			
		||||
                        completedChallengesIds.add(challengeHistory.id);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const PastWeeklyChallenges: ISeasonChallenge[] = [];
 | 
			
		||||
 | 
			
		||||
        let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1;
 | 
			
		||||
 | 
			
		||||
        while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) {
 | 
			
		||||
            const tempActs: ISeasonChallenge[] = [];
 | 
			
		||||
            pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason);
 | 
			
		||||
 | 
			
		||||
            for (const act of tempActs) {
 | 
			
		||||
                if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) {
 | 
			
		||||
                    if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) {
 | 
			
		||||
                        act.Permanent = true;
 | 
			
		||||
                    }
 | 
			
		||||
                    PastWeeklyChallenges.push(act);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            week--;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        res.json({ PastWeeklyChallenges: PastWeeklyChallenges });
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@ -1,33 +1,40 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { createGarden, getPersonalRooms } from "../../services/personalRoomsService.ts";
 | 
			
		||||
import type { IGetShipResponse, IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
 | 
			
		||||
import { getLoadout } from "../../services/loadoutService.ts";
 | 
			
		||||
import { toOid } from "../../helpers/inventoryHelpers.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { config } from "@/src/services/configService";
 | 
			
		||||
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getPersonalRooms } from "@/src/services/personalRoomsService";
 | 
			
		||||
import { getShip } from "@/src/services/shipService";
 | 
			
		||||
import { toOid } from "@/src/helpers/inventoryHelpers";
 | 
			
		||||
import { IGetShipResponse } from "@/src/types/shipTypes";
 | 
			
		||||
import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
 | 
			
		||||
import { getLoadout } from "@/src/services/loadoutService";
 | 
			
		||||
 | 
			
		||||
export const getShipController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const personalRoomsDb = await getPersonalRooms(accountId);
 | 
			
		||||
 | 
			
		||||
    // Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future.
 | 
			
		||||
    if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) {
 | 
			
		||||
        personalRoomsDb.Apartment.Gardening = createGarden();
 | 
			
		||||
        await personalRoomsDb.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
 | 
			
		||||
    const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
 | 
			
		||||
    const loadout = await getLoadout(accountId);
 | 
			
		||||
    const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
 | 
			
		||||
 | 
			
		||||
    const getShipResponse: IGetShipResponse = {
 | 
			
		||||
        ShipOwnerId: accountId,
 | 
			
		||||
        LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
 | 
			
		||||
        Ship: {
 | 
			
		||||
            ...personalRooms.Ship,
 | 
			
		||||
            ShipId: toOid(personalRoomsDb.activeShipId)
 | 
			
		||||
            ShipId: toOid(personalRoomsDb.activeShipId),
 | 
			
		||||
            ShipInterior: {
 | 
			
		||||
                Colors: personalRooms.ShipInteriorColors,
 | 
			
		||||
                ShipAttachments: ship.ShipAttachments,
 | 
			
		||||
                SkinFlavourItem: ship.SkinFlavourItem
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        Apartment: personalRooms.Apartment,
 | 
			
		||||
        TailorShop: personalRooms.TailorShop
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (config.unlockAllShipFeatures) {
 | 
			
		||||
        getShipResponse.Ship.Features = allShipFeatures;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    res.json(getShipResponse);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,29 +1,14 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "../../services/serversideVendorsService.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { config } from "../../services/configService.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
 | 
			
		||||
 | 
			
		||||
export const getVendorInfoController: RequestHandler = async (req, res) => {
 | 
			
		||||
    let manifest = getVendorManifestByTypeName(req.query.vendor as string);
 | 
			
		||||
export const getVendorInfoController: RequestHandler = (req, res) => {
 | 
			
		||||
    if (typeof req.query.vendor == "string") {
 | 
			
		||||
        const manifest = getVendorManifestByTypeName(req.query.vendor);
 | 
			
		||||
        if (!manifest) {
 | 
			
		||||
        throw new Error(`Unknown vendor: ${req.query.vendor 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);
 | 
			
		||||
    } else {
 | 
			
		||||
        res.status(400).end();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { crackRelic } from "../../helpers/relicHelper.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import type { IVoidTearParticipantInfo } from "../../types/requestTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { crackRelic } from "@/src/helpers/relicHelper";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IVoidTearParticipantInfo } from "@/src/types/requestTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
 | 
			
		||||
export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
@ -11,11 +11,7 @@ export const getVoidProjectionRewardsController: RequestHandler = async (req, re
 | 
			
		||||
 | 
			
		||||
    if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
 | 
			
		||||
        const inventory = await getInventory(accountId);
 | 
			
		||||
        const reward = await crackRelic(inventory, data.ParticipantInfo);
 | 
			
		||||
        if (!inventory.MissionRelicRewards || inventory.MissionRelicRewards.length >= data.CurrentWave) {
 | 
			
		||||
            inventory.MissionRelicRewards = [];
 | 
			
		||||
        }
 | 
			
		||||
        inventory.MissionRelicRewards.push({ ItemType: reward.type, ItemCount: reward.itemCount });
 | 
			
		||||
        await crackRelic(inventory, data.ParticipantInfo);
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,35 +1,16 @@
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { Account } from "../../models/loginModel.ts";
 | 
			
		||||
import { areFriends } from "../../services/friendService.ts";
 | 
			
		||||
import { createMessage } from "../../services/inboxService.ts";
 | 
			
		||||
import {
 | 
			
		||||
    combineInventoryChanges,
 | 
			
		||||
    getEffectiveAvatarImageType,
 | 
			
		||||
    getInventory,
 | 
			
		||||
    updateCurrency
 | 
			
		||||
} from "../../services/inventoryService.ts";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "../../services/loginService.ts";
 | 
			
		||||
import { handleDailyDealPurchase, handleStoreItemAcquisition } from "../../services/purchaseService.ts";
 | 
			
		||||
import type { IOid } from "../../types/commonTypes.ts";
 | 
			
		||||
import type { IPurchaseParams, IPurchaseResponse } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import { PurchaseSource } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
const checkPurchaseParams = (params: IPurchaseParams): boolean => {
 | 
			
		||||
    switch (params.Source) {
 | 
			
		||||
        case PurchaseSource.Market:
 | 
			
		||||
            return params.UsePremium;
 | 
			
		||||
 | 
			
		||||
        case PurchaseSource.DailyDeal:
 | 
			
		||||
            return true;
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
};
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { Account } from "@/src/models/loginModel";
 | 
			
		||||
import { createMessage } from "@/src/services/inboxService";
 | 
			
		||||
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { IPurchaseParams } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportFlavour } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const data = getJSONfromString<IGiftingRequest>(String(req.body));
 | 
			
		||||
    if (!checkPurchaseParams(data.PurchaseParams)) {
 | 
			
		||||
    if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) {
 | 
			
		||||
        throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -49,11 +30,8 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Cannot gift to players who have gifting disabled.
 | 
			
		||||
    const senderAccount = await getAccountForRequest(req);
 | 
			
		||||
    if (
 | 
			
		||||
        inventory.Settings?.GiftMode == "GIFT_MODE_NONE" ||
 | 
			
		||||
        (inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id)))
 | 
			
		||||
    ) {
 | 
			
		||||
    // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
 | 
			
		||||
    if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") {
 | 
			
		||||
        res.status(400).send("17").end();
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
@ -62,7 +40,11 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
    // TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7)
 | 
			
		||||
    // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20)
 | 
			
		||||
 | 
			
		||||
    const senderInventory = await getInventory(senderAccount._id.toString());
 | 
			
		||||
    const senderAccount = await getAccountForRequest(req);
 | 
			
		||||
    const senderInventory = await getInventory(
 | 
			
		||||
        senderAccount._id.toString(),
 | 
			
		||||
        "PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (senderInventory.GiftsRemaining == 0) {
 | 
			
		||||
        res.status(400).send("10").end();
 | 
			
		||||
@ -70,23 +52,7 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }
 | 
			
		||||
    senderInventory.GiftsRemaining -= 1;
 | 
			
		||||
 | 
			
		||||
    const response: IPurchaseResponse = {
 | 
			
		||||
        InventoryChanges: {}
 | 
			
		||||
    };
 | 
			
		||||
    if (data.PurchaseParams.Source == PurchaseSource.DailyDeal) {
 | 
			
		||||
        await handleDailyDealPurchase(senderInventory, data.PurchaseParams, response);
 | 
			
		||||
    } else {
 | 
			
		||||
        updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true, response.InventoryChanges);
 | 
			
		||||
    }
 | 
			
		||||
    if (data.PurchaseParams.StoreItem in ExportBundles) {
 | 
			
		||||
        const bundle = ExportBundles[data.PurchaseParams.StoreItem];
 | 
			
		||||
        if (bundle.giftingBonus) {
 | 
			
		||||
            combineInventoryChanges(
 | 
			
		||||
                response.InventoryChanges,
 | 
			
		||||
                (await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true);
 | 
			
		||||
    await senderInventory.save();
 | 
			
		||||
 | 
			
		||||
    const senderName = getSuffixedName(senderAccount);
 | 
			
		||||
@ -105,7 +71,7 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
                }
 | 
			
		||||
            ],
 | 
			
		||||
            sub: "/Lotus/Language/Menu/GiftReceivedSubject",
 | 
			
		||||
            icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon,
 | 
			
		||||
            icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
 | 
			
		||||
            gifts: [
 | 
			
		||||
                {
 | 
			
		||||
                    GiftType: data.PurchaseParams.StoreItem
 | 
			
		||||
@ -114,7 +80,7 @@ export const giftingController: RequestHandler = async (req, res) => {
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    res.json(response);
 | 
			
		||||
    res.end();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IGiftingRequest {
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,19 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { addMiscItems, getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
 | 
			
		||||
import type { ArtifactPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { WeaponTypeInternal } from "@/src/services/itemDataService";
 | 
			
		||||
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
 | 
			
		||||
import { ExportRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
 | 
			
		||||
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
 | 
			
		||||
import { EquipmentFeatures } from "../../types/equipmentTypes.ts";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
 | 
			
		||||
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
 | 
			
		||||
    "LongGuns",
 | 
			
		||||
    "Pistols",
 | 
			
		||||
    "Melee",
 | 
			
		||||
    "OperatorAmps",
 | 
			
		||||
    "Hoverboards"
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
interface IGildWeaponRequest {
 | 
			
		||||
    ItemName: string;
 | 
			
		||||
@ -16,14 +21,17 @@ interface IGildWeaponRequest {
 | 
			
		||||
    PolarizeSlot?: number;
 | 
			
		||||
    PolarizeValue?: ArtifactPolarity;
 | 
			
		||||
    ItemId: string;
 | 
			
		||||
    Category: TEquipmentKey;
 | 
			
		||||
    Category: WeaponTypeInternal | "Hoverboards";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
 | 
			
		||||
    data.ItemId = String(req.query.ItemId);
 | 
			
		||||
    data.Category = req.query.Category as TEquipmentKey;
 | 
			
		||||
    if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
 | 
			
		||||
        throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
 | 
			
		||||
    }
 | 
			
		||||
    data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
 | 
			
		||||
@ -34,10 +42,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const weapon = inventory[data.Category][weaponIndex];
 | 
			
		||||
    weapon.Features ??= 0;
 | 
			
		||||
    weapon.Features |= EquipmentFeatures.GILDED;
 | 
			
		||||
    if (data.Recipe != "webui") {
 | 
			
		||||
    weapon.ItemName = data.ItemName;
 | 
			
		||||
    weapon.XP = 0;
 | 
			
		||||
    }
 | 
			
		||||
    if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
 | 
			
		||||
        weapon.Polarity = [
 | 
			
		||||
            {
 | 
			
		||||
@ -50,9 +56,6 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
    inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
 | 
			
		||||
 | 
			
		||||
    const affiliationMods = [];
 | 
			
		||||
 | 
			
		||||
    if (data.Recipe != "webui") {
 | 
			
		||||
    const recipe = ExportRecipes[data.Recipe];
 | 
			
		||||
    inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
 | 
			
		||||
        ItemType: ingredient.ItemType,
 | 
			
		||||
@ -60,6 +63,7 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
    }));
 | 
			
		||||
    addMiscItems(inventory, inventoryChanges.MiscItems);
 | 
			
		||||
 | 
			
		||||
    const affiliationMods = [];
 | 
			
		||||
    if (recipe.syndicateStandingChange) {
 | 
			
		||||
        const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
 | 
			
		||||
        affiliation.Standing += recipe.syndicateStandingChange.value;
 | 
			
		||||
@ -68,12 +72,10 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
 | 
			
		||||
            Standing: recipe.syndicateStandingChange.value
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges,
 | 
			
		||||
        AffiliationMods: affiliationMods
 | 
			
		||||
    });
 | 
			
		||||
    broadcastInventoryUpdate(req);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,17 +1,16 @@
 | 
			
		||||
import type { RequestHandler } from "express";
 | 
			
		||||
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
 | 
			
		||||
import { getInventory } from "../../services/inventoryService.ts";
 | 
			
		||||
import { giveKeyChainItem } from "../../services/questService.ts";
 | 
			
		||||
import type { IKeyChainRequest } from "../../types/requestTypes.ts";
 | 
			
		||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { parseString } from "@/src/helpers/general";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { giveKeyChainItem } from "@/src/services/questService";
 | 
			
		||||
import { IKeyChainRequest } from "@/src/types/requestTypes";
 | 
			
		||||
 | 
			
		||||
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const accountId = parseString(req.query.accountId);
 | 
			
		||||
    const keyChainInfo = getJSONfromString<IKeyChainRequest>((req.body as string).toString());
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain)!;
 | 
			
		||||
    const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo, questKey);
 | 
			
		||||
    const inventoryChanges = await giveKeyChainItem(inventory, keyChainInfo);
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
 | 
			
		||||
    res.send(inventoryChanges);
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user