merge upstream

This commit is contained in:
Animan8000 2025-07-09 01:25:37 -07:00
commit 2e6188109c
202 changed files with 11523 additions and 10392 deletions

View File

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

View File

@ -1,6 +1,7 @@
name: Build name: Build
on: on:
push: {} push:
branches: ["main"]
pull_request: {} pull_request: {}
jobs: jobs:
build: build:
@ -18,6 +19,7 @@ jobs:
- run: npm run lint:ci - run: npm run lint:ci
- run: npm run prettier - run: npm run prettier
- run: npm run update-translations - run: npm run update-translations
- run: npm run fix-imports
- name: Fail if there are uncommitted changes - name: Fail if there are uncommitted changes
run: | run: |
if [[ -n "$(git status --porcelain)" ]]; then if [[ -n "$(git status --porcelain)" ]]; then

View File

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

3
.vscode/launch.json vendored
View File

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

17
AGENTS.md Normal file
View File

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

View File

@ -1,53 +1,11 @@
FROM node:18-alpine3.19 FROM node:24-alpine3.21
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF RUN apk add --no-cache bash jq
ENV APP_MY_ADDRESS=localhost
ENV APP_HTTP_PORT=80
ENV APP_HTTPS_PORT=443
ENV APP_AUTO_CREATE_ACCOUNT=true
ENV APP_SKIP_TUTORIAL=false
ENV APP_SKIP_ALL_DIALOGUE=false
ENV APP_UNLOCK_ALL_SCANS=false
ENV APP_UNLOCK_ALL_MISSIONS=false
ENV APP_INFINITE_CREDITS=false
ENV APP_INFINITE_PLATINUM=false
ENV APP_INFINITE_ENDO=false
ENV APP_INFINITE_REGAL_AYA=false
ENV APP_INFINITE_HELMINTH_MATERIALS=false
ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false
ENV APP_DONT_SUBTRACT_VOIDTRACES=false
ENV APP_DONT_SUBTRACT_CONSUMABLES=false
ENV APP_UNLOCK_ALL_SHIP_FEATURES=false
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false
ENV APP_UNLOCK_ALL_FLAVOUR_ITEMS=false
ENV APP_UNLOCK_ALL_SKINS=false
ENV APP_UNLOCK_ALL_CAPTURA_SCENES=false
ENV APP_UNIVERSAL_POLARITY_EVERYWHERE=false
ENV APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE=false
ENV APP_UNLOCK_EXILUS_EVERYWHERE=false
ENV APP_UNLOCK_ARCANES_EVERYWHERE=false
ENV APP_NO_DAILY_FOCUS_LIMIT=false
ENV APP_NO_ARGON_CRYSTAL_DECAY=false
ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false
ENV APP_NO_VENDOR_PURCHASE_LIMITS=true
ENV APP_NO_DEATH_MARKS=false
ENV APP_NO_KIM_COOLDOWNS=false
ENV APP_SYNDICATE_MISSIONS_REPEATABLE=false
ENV APP_INSTANT_FINISH_RIVEN_CHALLENGE=false
ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false
ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false
ENV APP_SKIP_CLAN_KEY_CRAFTING=false
ENV APP_NO_DOJO_ROOM_BUILD_STAGE=false
ENV APP_NO_DECO_BUILD_STAGE=false
ENV APP_FAST_DOJO_ROOM_DESTRUCTION=false
ENV APP_NO_DOJO_RESEARCH_COSTS=false
ENV APP_NO_DOJO_RESEARCH_TIME=false
ENV APP_FAST_CLAN_ASCENSION=false
ENV APP_SPOOF_MASTERY_RANK=-1
RUN apk add --no-cache bash sed wget jq
COPY . /app COPY . /app
WORKDIR /app WORKDIR /app
RUN npm i --omit=dev
RUN npm run build
ENTRYPOINT ["/app/docker-entrypoint.sh"] ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -16,6 +16,7 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift. - `worldState.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.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: - `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9 - `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8 - `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
@ -33,3 +34,5 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `RadioLegion2Syndicate` for The Emissary - `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I - `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six - `RadioLegionSyndicate` for The Wolf of Saturn Six
- `allTheFissures` can be set to `normal` or `hard` to enable all fissures either in normal or steel path, respectively.
- `worldState.circuitGameModes` can be set to an array of game modes which will override the otherwise-random pattern in The Circuit. Valid element values are `Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, and `Alchemy`.

View File

@ -13,13 +13,16 @@
"skipTutorial": false, "skipTutorial": false,
"skipAllDialogue": false, "skipAllDialogue": false,
"unlockAllScans": false, "unlockAllScans": false,
"unlockAllMissions": false,
"infiniteCredits": false, "infiniteCredits": false,
"infinitePlatinum": false, "infinitePlatinum": false,
"infiniteEndo": false, "infiniteEndo": false,
"infiniteRegalAya": false, "infiniteRegalAya": false,
"infiniteHelminthMaterials": false, "infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false, "claimingBlueprintRefundsIngredients": false,
"dontSubtractPurchaseCreditCost": false,
"dontSubtractPurchasePlatinumCost": false,
"dontSubtractPurchaseItemCost": false,
"dontSubtractPurchaseStandingCost": false,
"dontSubtractVoidTraces": false, "dontSubtractVoidTraces": false,
"dontSubtractConsumables": false, "dontSubtractConsumables": false,
"unlockAllShipFeatures": false, "unlockAllShipFeatures": false,
@ -35,10 +38,14 @@
"noDailyFocusLimit": false, "noDailyFocusLimit": false,
"noArgonCrystalDecay": false, "noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false, "noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true, "noVendorPurchaseLimits": false,
"noDeathMarks": false, "noDeathMarks": false,
"noKimCooldowns": false, "noKimCooldowns": false,
"fullyStockedVendors": false,
"baroAlwaysAvailable": false,
"baroFullyStocked": false,
"syndicateMissionsRepeatable": false, "syndicateMissionsRepeatable": false,
"unlockAllProfitTakerStages": false,
"instantFinishRivenChallenge": false, "instantFinishRivenChallenge": false,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false, "noResourceExtractorDronesDamage": false,
@ -49,14 +56,33 @@
"noDojoResearchCosts": false, "noDojoResearchCosts": false,
"noDojoResearchTime": false, "noDojoResearchTime": false,
"fastClanAscension": false, "fastClanAscension": false,
"missionsCanGiveAllRelics": false,
"unlockAllSimarisResearchEntries": false,
"disableDailyTribute": false,
"spoofMasteryRank": -1, "spoofMasteryRank": -1,
"relicRewardItemCountMultiplier": 1,
"nightwaveStandingMultiplier": 1,
"unfaithfulBugFixes": {
"ignore1999LastRegionPlayed": false,
"fixXtraCheeseTimer": false
},
"worldState": { "worldState": {
"creditBoost": false, "creditBoost": false,
"affinityBoost": false, "affinityBoost": false,
"resourceBoost": false, "resourceBoost": false,
"starDays": true, "starDays": true,
"galleonOfGhouls": 0,
"eidolonOverride": "", "eidolonOverride": "",
"vallisOverride": "", "vallisOverride": "",
"nightwaveOverride": "" "duviriOverride": "",
"nightwaveOverride": "",
"allTheFissures": "",
"circuitGameModes": null,
"darvoStockMultiplier": 1,
"varziaOverride": "",
"varziaFullyStocked": false
},
"dev": {
"keepVendorsExpired": false
} }
} }

View File

@ -1,62 +1,20 @@
services: services:
spaceninjaserver: spaceninjaserver:
# build: . # The image to use. If you have an ARM CPU, replace 'latest' with 'latest-arm64'.
image: openwf/spaceninjaserver:latest image: openwf/spaceninjaserver:latest
environment:
APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/
# Following environment variables are set to default image values.
# Uncomment to edit.
# APP_MY_ADDRESS: localhost
# APP_HTTP_PORT: 80
# APP_HTTPS_PORT: 443
# APP_AUTO_CREATE_ACCOUNT: true
# APP_SKIP_TUTORIAL: false
# APP_SKIP_ALL_DIALOGUE: false
# APP_UNLOCK_ALL_SCANS: false
# APP_UNLOCK_ALL_MISSIONS: false
# APP_INFINITE_CREDITS: false
# APP_INFINITE_PLATINUM: false
# APP_INFINITE_ENDO: false
# APP_INFINITE_REGAL_AYA: false
# APP_INFINITE_HELMINTH_MATERIALS: false
# APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false
# APP_DONT_SUBTRACT_VOIDTRACES: false
# APP_DONT_SUBTRACT_CONSUMABLES: false
# APP_UNLOCK_ALL_SHIP_FEATURES: false
# APP_UNLOCK_ALL_SHIP_DECORATIONS: false
# APP_UNLOCK_ALL_FLAVOUR_ITEMS: false
# APP_UNLOCK_ALL_SKINS: false
# APP_UNLOCK_ALL_CAPTURA_SCENES: false
# APP_UNIVERSAL_POLARITY_EVERYWHERE: false
# APP_UNLOCK_DOUBLE_CAPACITY_POTATOES_EVERYWHERE: false
# APP_UNLOCK_EXILUS_EVERYWHERE: false
# APP_UNLOCK_ARCANES_EVERYWHERE: false
# APP_NO_DAILY_FOCUS_LIMIT: false
# APP_NO_ARGON_CRYSTAL_DECAY: false
# APP_NO_MASTERY_RANK_UP_COOLDOWN: false
# APP_NO_VENDOR_PURCHASE_LIMITS: true
# APP_NO_DEATH_MARKS: false
# APP_NO_KIM_COOLDOWNS: false
# APP_SYNDICATE_MISSIONS_REPEATABLE: false
# APP_INSTANT_FINISH_RIVEN_CHALLENGE: false
# APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false
# APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false
# APP_SKIP_CLAN_KEY_CRAFTING: false
# APP_NO_DOJO_ROOM_BUILD_STAGE: false
# APP_NO_DECO_BUILD_STAGE: false
# APP_FAST_DOJO_ROOM_DESTRUCTION: false
# APP_NO_DOJO_RESEARCH_COSTS: false
# APP_NO_DOJO_RESEARCH_TIME: false
# APP_FAST_CLAN_ASCENSION: false
# APP_SPOOF_MASTERY_RANK: -1
volumes: volumes:
- ./docker-data/static:/app/static/data - ./docker-data/conf:/app/conf
- ./docker-data/static-data:/app/static/data
- ./docker-data/logs:/app/logs - ./docker-data/logs:/app/logs
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
# Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
#build: .
# Works best when using `docker-compose up --force-recreate --build`.
depends_on: depends_on:
- mongodb - mongodb
mongodb: mongodb:
@ -66,3 +24,4 @@ services:
MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver MONGO_INITDB_ROOT_PASSWORD: spaceninjaserver
volumes: volumes:
- ./docker-data/database:/data/db - ./docker-data/database:/data/db
command: mongod --quiet --logpath /dev/null

View File

@ -1,24 +1,8 @@
#!/bin/bash #!/bin/bash
set -e set -e
# Set up the configuration file using environment variables. if [ ! -f conf/config.json ]; then
echo '{ jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config.json.example > /app/conf/config.json
"logger": { fi
"files": true,
"level": "trace",
"__valid_levels": "fatal, error, warn, info, http, debug, trace"
}
}
' > config.json
for config in $(env | grep "APP_") exec npm run start -- --configPath conf/config.json
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 i --omit=dev
npm run build
exec npm run start

2709
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,19 +5,32 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js", "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "build": "tsgo --sourceMap && ncp static/webui build/static/webui",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui", "build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"build:dev": "tsgo --sourceMap",
"build:dev:tsc": "tsc --incremental --sourceMap",
"build-and-start": "npm run build && npm run start",
"build-and-start:bun": "npm run verify && npm run bun-run",
"dev": "node scripts/dev.js",
"dev:bun": "bun scripts/dev.js",
"verify": "tsgo --noEmit", "verify": "tsgo --noEmit",
"bun-run": "bun src/index.ts",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .", "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .", "prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js" "update-translations": "cd scripts && node update-translations.js",
"fix-imports": "cd scripts && node fix-imports.js",
"fix": "npm run update-translations && npm run fix-imports && npm run prettier"
}, },
"license": "GNU", "license": "GNU",
"dependencies": { "dependencies": {
"@types/express": "^5", "@types/express": "^5",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/websocket": "^1.0.10",
"@types/ws": "^8.18.1",
"@typescript/native-preview": "^7.0.0-dev.20250625.1",
"chokidar": "^4.0.3",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.4.4", "json-with-bigint": "^3.4.4",
@ -25,19 +38,21 @@
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.66", "undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.77",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0",
"ws": "^8.18.2"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.28.0",
"@typescript/native-preview": "^7.0.0-dev.20250523.1",
"eslint": "^8", "eslint": "^8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.2.5", "eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"ts-node-dev": "^2.0.0", "tree-kill": "^1.2.2"
"tsconfig-paths": "^4.2.0"
} }
} }

58
scripts/dev.js Normal file
View File

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

46
scripts/fix-imports.js Normal file
View File

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

View File

@ -1,6 +1,7 @@
// Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php // Based on https://onlyg.it/OpenWF/Translations/src/branch/main/update.php
// Converted via ChatGPT-4o // Converted via ChatGPT-4o
/* eslint-disable */
const fs = require("fs"); const fs = require("fs");
function extractStrings(content) { function extractStrings(content) {
@ -30,7 +31,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
const strings = extractStrings(line); const strings = extractStrings(line);
if (Object.keys(strings).length > 0) { if (Object.keys(strings).length > 0) {
Object.entries(strings).forEach(([key, value]) => { Object.entries(strings).forEach(([key, value]) => {
if (targetStrings.hasOwnProperty(key)) { if (targetStrings.hasOwnProperty(key) && !targetStrings[key].startsWith("[UNTRANSLATED] ")) {
fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`${targetStrings[key]}\`,\n`);
} else { } else {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);

View File

@ -24,7 +24,6 @@ export const artifactsController: RequestHandler = async (req, res) => {
if (itemIndex !== -1) { if (itemIndex !== -1) {
Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint; Upgrades[itemIndex].UpgradeFingerprint = stringifiedUpgradeFingerprint;
inventory.markModified(`Upgrades.${itemIndex}.UpgradeFingerprint`);
} else { } else {
itemIndex = itemIndex =
Upgrades.push({ Upgrades.push({

View File

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

View File

@ -13,15 +13,18 @@ import {
addItem, addItem,
addRecipes, addRecipes,
occupySlot, occupySlot,
combineInventoryChanges combineInventoryChanges,
addKubrowPetPrint,
addPowerSuit,
addEquipment
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { toOid2 } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus"; import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { EquipmentFeatures, IEquipmentClient, Status } from "@/src/types/equipmentTypes";
interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
@ -119,18 +122,126 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
} }
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis; pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
addKubrowPetPrint(inventory, pet, InventoryChanges);
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") { } else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = { if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
...InventoryChanges, // Quite the special case here...
...(await addItem( // We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
// Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
const umbraModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModC = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
InventoryChanges.Upgrades ??= [];
InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
await addPowerSuit(
inventory, inventory,
recipe.resultType, "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
recipe.num, {
false, Configs: [
undefined, {
pendingRecipe.TargetFingerprint Upgrades: [
)) "",
}; "",
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
]
}
],
XP: 900_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
XP: 900_000
});
addEquipment(
inventory,
"Melee",
"/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
{
Configs: [
{ Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
],
XP: 450_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
XP: 450_000
});
} else {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
};
}
} }
if ( if (
config.claimingBlueprintRefundsIngredients && config.claimingBlueprintRefundsIngredients &&

View File

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

View File

@ -1,4 +1,4 @@
import { getCalendarProgress, getInventory } from "@/src/services/inventoryService"; import { checkCalendarAutoAdvance, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getWorldState } from "@/src/services/worldStateService"; import { getWorldState } from "@/src/services/worldStateService";
@ -12,27 +12,23 @@ export const completeCalendarEventController: RequestHandler = async (req, res)
const calendarProgress = getCalendarProgress(inventory); const calendarProgress = getCalendarProgress(inventory);
const currentSeason = getWorldState().KnownCalendarSeasons[0]; const currentSeason = getWorldState().KnownCalendarSeasons[0];
let inventoryChanges: IInventoryChanges = {}; let inventoryChanges: IInventoryChanges = {};
let dayIndex = 0; const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
for (const day of currentSeason.Days) { const day = currentSeason.Days[dayIndex];
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") { if (day.events.length != 0) {
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) { if (day.events[0].type == "CET_CHALLENGE") {
if (day.events.length != 0) { 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") { const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)) if (selection.type == "CET_REWARD") {
.InventoryChanges; inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory)).InventoryChanges;
} else if (selection.type == "CET_UPGRADE") { } else if (selection.type == "CET_UPGRADE") {
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!); calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
} else if (selection.type != "CET_PLOT") { } else if (selection.type != "CET_PLOT") {
throw new Error(`unexpected selection type: ${selection.type}`); throw new Error(`unexpected selection type: ${selection.type}`);
}
}
break;
}
++dayIndex;
} }
} }
calendarProgress.SeasonProgress.LastCompletedDayIdx++; calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
checkCalendarAutoAdvance(inventory, currentSeason);
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,

View File

@ -21,7 +21,8 @@ import {
updateCurrency updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { ITypeCount } from "@/src/types/commonTypes";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const contributeToVaultController: RequestHandler = async (req, res) => { export const contributeToVaultController: RequestHandler = async (req, res) => {

View File

@ -4,9 +4,15 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
export const creditsController: RequestHandler = async (req, res) => { export const creditsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const inventory = (
await Promise.all([
const inventory = await getInventory(accountId, "RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits"); getAccountIdForRequest(req),
getInventory(
req.query.accountId as string,
"RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits"
)
])
)[1];
const response = { const response = {
RegularCredits: inventory.RegularCredits, RegularCredits: inventory.RegularCredits,

View File

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

View File

@ -12,7 +12,7 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService"; import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper"; import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentDatabase } from "@/src/types/equipmentTypes";
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => { export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);

View File

@ -1,12 +1,15 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService"; import { hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => { export const customObstacleCourseLeaderboardController: RequestHandler = async (req, res) => {
const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body)); const data = getJSONfromString<ICustomObstacleCourseLeaderboardRequest>(String(req.body));
const guild = (await Guild.findById(data.g, "DojoComponents"))!; const guild = (await Guild.findById(data.g, "DojoComponents Ranks"))!;
const component = guild.DojoComponents.id(data.c)!; const component = guild.DojoComponents.id(data.c)!;
if (req.query.act == "f") { if (req.query.act == "f") {
res.json({ res.json({
@ -34,6 +37,19 @@ export const customObstacleCourseLeaderboardController: RequestHandler = async (
entry.r = ++r; entry.r = ++r;
} }
await guild.save(); 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(); res.status(200).end();
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); logger.debug(`data provided to ${req.path}: ${String(req.body)}`);

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import { getInventory, addMiscItems, addEquipment, occupySlot } from "@/src/serv
import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, TFocusPolarity, TEquipmentKey, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFocusUpgrades } from "warframe-public-export-plus"; import { ExportFocusUpgrades } from "warframe-public-export-plus";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const focusController: RequestHandler = async (req, res) => { export const focusController: RequestHandler = async (req, res) => {
@ -43,7 +42,7 @@ export const focusController: RequestHandler = async (req, res) => {
inventory.FocusAbility ??= focusType; inventory.FocusAbility ??= focusType;
inventory.FocusUpgrades.push({ ItemType: focusType }); inventory.FocusUpgrades.push({ ItemType: focusType });
if (inventory.FocusXP) { if (inventory.FocusXP) {
inventory.FocusXP[focusPolarity] -= cost; inventory.FocusXP[focusPolarity]! -= cost;
} }
await inventory.save(); await inventory.save();
res.json({ res.json({
@ -78,7 +77,7 @@ export const focusController: RequestHandler = async (req, res) => {
cost += ExportFocusUpgrades[focusType].baseFocusPointCost; cost += ExportFocusUpgrades[focusType].baseFocusPointCost;
inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 }); inventory.FocusUpgrades.push({ ItemType: focusType, Level: 0 });
} }
inventory.FocusXP![focusPolarity] -= cost; inventory.FocusXP![focusPolarity]! -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusTypes: request.FocusTypes, FocusTypes: request.FocusTypes,
@ -96,7 +95,7 @@ export const focusController: RequestHandler = async (req, res) => {
const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!; const focusUpgradeDb = inventory.FocusUpgrades.find(entry => entry.ItemType == focusUpgrade.ItemType)!;
focusUpgradeDb.Level = focusUpgrade.Level; focusUpgradeDb.Level = focusUpgrade.Level;
} }
inventory.FocusXP![focusPolarity] -= cost; inventory.FocusXP![focusPolarity]! -= cost;
await inventory.save(); await inventory.save();
res.json({ res.json({
FocusInfos: request.FocusInfos, FocusInfos: request.FocusInfos,
@ -116,14 +115,14 @@ export const focusController: RequestHandler = async (req, res) => {
}); });
occupySlot(inventory, InventorySlot.AMPS, false); occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save(); await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]); res.json(inventoryChanges.OperatorAmps![0]);
break; break;
} }
case FocusOperation.UnbindUpgrade: { case FocusOperation.UnbindUpgrade: {
const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest; const request = JSON.parse(String(req.body)) as IUnbindUpgradeRequest;
const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]); const focusPolarity = focusTypeToPolarity(request.FocusTypes[0]);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.FocusXP![focusPolarity] -= 750_000 * request.FocusTypes.length; inventory.FocusXP![focusPolarity]! -= 750_000 * request.FocusTypes.length;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem", ItemType: "/Lotus/Types/Gameplay/Eidolon/Resources/SentientShards/SentientShardBrilliantItem",
@ -168,8 +167,10 @@ export const focusController: RequestHandler = async (req, res) => {
shard.ItemCount *= -1; shard.ItemCount *= -1;
} }
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
inventory.FocusXP ??= { AP_POWER: 0, AP_TACTIC: 0, AP_DEFENSE: 0, AP_ATTACK: 0, AP_WARD: 0 }; const polarity = request.Polarity;
inventory.FocusXP[request.Polarity] += xp; inventory.FocusXP ??= {};
inventory.FocusXP[polarity] ??= 0;
inventory.FocusXP[polarity] += xp;
addMiscItems(inventory, request.Shards); addMiscItems(inventory, request.Shards);
await inventory.save(); await inventory.save();
break; break;

View File

@ -6,9 +6,8 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { IMissionReward } from "@/src/types/missionTypes"; import { IMissionReward } from "@/src/types/missionTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { IGardeningClient, IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IGardeningClient } from "@/src/types/shipTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { dict_en, ExportResources } from "warframe-public-export-plus"; import { dict_en, ExportResources } from "warframe-public-export-plus";

View File

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

View File

@ -1,6 +1,6 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { generateRewardSeed } from "@/src/services/rngService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => { export const getNewRewardSeedController: RequestHandler = async (req, res) => {

View File

@ -3,10 +3,9 @@ import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { toOid } from "@/src/helpers/inventoryHelpers"; import { IGetShipResponse, IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService"; import { getLoadout } from "@/src/services/loadoutService";
import { toOid } from "@/src/helpers/inventoryHelpers";
export const getShipController: RequestHandler = async (req, res) => { export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -26,15 +25,7 @@ export const getShipController: RequestHandler = async (req, res) => {
LoadOutInventory: { LoadOutPresets: loadout.toJSON() }, LoadOutInventory: { LoadOutPresets: loadout.toJSON() },
Ship: { Ship: {
...personalRooms.Ship, ...personalRooms.Ship,
ShipId: toOid(personalRoomsDb.activeShipId), ShipId: toOid(personalRoomsDb.activeShipId)
ShipInterior: {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: { HOOD_ORNAMENT: "" },
SkinFlavourItem: ""
},
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)
: undefined
}, },
Apartment: personalRooms.Apartment, Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop TailorShop: personalRooms.TailorShop

View File

@ -2,6 +2,7 @@ import { RequestHandler } from "express";
import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService"; import { applyStandingToVendorManifest, getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { config } from "@/src/services/configService";
export const getVendorInfoController: RequestHandler = async (req, res) => { export const getVendorInfoController: RequestHandler = async (req, res) => {
let manifest = getVendorManifestByTypeName(req.query.vendor as string); let manifest = getVendorManifestByTypeName(req.query.vendor as string);
@ -14,6 +15,14 @@ export const getVendorInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
manifest = applyStandingToVendorManifest(inventory, manifest); manifest = applyStandingToVendorManifest(inventory, manifest);
if (config.dev?.keepVendorsExpired) {
manifest = {
VendorInfo: {
...manifest.VendorInfo,
Expiry: { $date: { $numberLong: "0" } }
}
};
}
} }
res.json(manifest); res.json(manifest);

View File

@ -9,15 +9,26 @@ import {
updateCurrency updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleDailyDealPurchase, handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes"; import { IPurchaseParams, IPurchaseResponse, PurchaseSource } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus"; import { ExportBundles, ExportFlavour } from "warframe-public-export-plus";
const checkPurchaseParams = (params: IPurchaseParams): boolean => {
switch (params.Source) {
case PurchaseSource.Market:
return params.UsePremium;
case PurchaseSource.DailyDeal:
return true;
}
return false;
};
export const giftingController: RequestHandler = async (req, res) => { export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body)); const data = getJSONfromString<IGiftingRequest>(String(req.body));
if (data.PurchaseParams.Source != 0 || !data.PurchaseParams.UsePremium) { if (!checkPurchaseParams(data.PurchaseParams)) {
throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`); throw new Error(`unexpected purchase params in gifting request: ${String(req.body)}`);
} }
@ -58,16 +69,19 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
senderInventory.GiftsRemaining -= 1; senderInventory.GiftsRemaining -= 1;
const inventoryChanges: IInventoryChanges = updateCurrency( const response: IPurchaseResponse = {
senderInventory, InventoryChanges: {}
data.PurchaseParams.ExpectedPrice, };
true 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) { if (data.PurchaseParams.StoreItem in ExportBundles) {
const bundle = ExportBundles[data.PurchaseParams.StoreItem]; const bundle = ExportBundles[data.PurchaseParams.StoreItem];
if (bundle.giftingBonus) { if (bundle.giftingBonus) {
combineInventoryChanges( combineInventoryChanges(
inventoryChanges, response.InventoryChanges,
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges (await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
); );
} }
@ -99,9 +113,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
]); ]);
res.json({ res.json(response);
InventoryChanges: inventoryChanges
});
}; };
interface IGiftingRequest { interface IGiftingRequest {

View File

@ -3,9 +3,10 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus"; import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures, IEquipmentClient } from "@/src/types/equipmentTypes";
interface IGildWeaponRequest { interface IGildWeaponRequest {
ItemName: string; ItemName: string;

View File

@ -1,7 +1,8 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService"; import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { ITypeCount } from "@/src/types/commonTypes";
import { ILoreFragmentScan } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => { export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {

View File

@ -5,13 +5,14 @@ import {
getGuildVault, getGuildVault,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
processCompletedGuildTechProject,
processFundedGuildTechProject, processFundedGuildTechProject,
processGuildTechProjectContributionsUpdate, processGuildTechProjectContributionsUpdate,
removePigmentsFromGuildMembers, removePigmentsFromGuildMembers,
scaleRequiredCount, scaleRequiredCount,
setGuildTechLogState setGuildTechLogState
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
addCrewShipWeaponSkin, addCrewShipWeaponSkin,
@ -51,8 +52,12 @@ export const guildTechController: RequestHandler = async (req, res) => {
}; };
if (project.CompletionDate) { if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate); techProject.CompletionDate = toMongoDate(project.CompletionDate);
if (Date.now() >= project.CompletionDate.getTime()) { if (
needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate); Date.now() >= project.CompletionDate.getTime() &&
setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate)
) {
processCompletedGuildTechProject(guild, project.ItemType);
needSave = true;
} }
} }
techProjects.push(techProject); techProjects.push(techProject);
@ -442,6 +447,7 @@ const finishComponentRepair = (
...(category == "CrewShipWeaponSkins" ...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint) ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(inventory, category, salvageItem.ItemType, { : addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
UpgradeFingerprint: salvageItem.UpgradeFingerprint UpgradeFingerprint: salvageItem.UpgradeFingerprint
})), })),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false) ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)

View File

@ -30,8 +30,9 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(account._id.toString());
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { suit.ArchonCrystalUpgrades ??= [];
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; while (suit.ArchonCrystalUpgrades.length < request.Slot) {
suit.ArchonCrystalUpgrades.push({});
} }
suit.ArchonCrystalUpgrades[request.Slot] = { suit.ArchonCrystalUpgrades[request.Slot] = {
UpgradeType: request.UpgradeType, UpgradeType: request.UpgradeType,
@ -92,7 +93,8 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {}; suit.ArchonCrystalUpgrades![request.Slot].UpgradeType = undefined;
suit.ArchonCrystalUpgrades![request.Slot].Color = undefined;
await inventory.save(); await inventory.save();

View File

@ -5,30 +5,32 @@ import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json"; import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { import { ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus";
ExportCustoms,
ExportFlavour,
ExportRegions,
ExportResources,
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { import {
addEmailItem,
addMiscItems, addMiscItems,
allDailyAffiliationKeys, allDailyAffiliationKeys,
checkCalendarAutoAdvance,
cleanupInventory, cleanupInventory,
createLibraryDailyTask, createLibraryDailyTask,
generateRewardSeed getCalendarProgress
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers"; import { addString, catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers"; import { getNemesisManifest } from "@/src/helpers/nemesisHelpers";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel"; import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers"; import { toLegacyOid, toOid, version_compare } from "@/src/helpers/inventoryHelpers";
import { Inbox } from "@/src/models/inboxModel";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { DailyDeal } from "@/src/models/worldStateModel";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
import { generateRewardSeed } from "@/src/services/rngService";
import { getWorldState } from "@/src/services/worldStateService";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const account = await getAccountForRequest(request);
@ -42,6 +44,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
// Handle daily reset // Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) { if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
const today = Math.trunc(Date.now() / 86400000);
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500; inventory[key] = 16000 + inventory.PlayerLevel * 500;
} }
@ -52,12 +56,12 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
if (inventory.NextRefill) { if (inventory.NextRefill) {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const daysPassed = today - lastLoginDay;
if (config.noArgonCrystalDecay) { if (config.noArgonCrystalDecay) {
inventory.FoundToday = undefined; inventory.FoundToday = undefined;
} else { } else {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const today = Math.trunc(Date.now() / 86400000);
const daysPassed = today - lastLoginDay;
for (let i = 0; i != daysPassed; ++i) { for (let i = 0; i != daysPassed; ++i) {
const numArgonCrystals = const numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
@ -89,11 +93,87 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory.FoundToday = undefined; inventory.FoundToday = undefined;
} }
} }
if (inventory.UsedDailyDeals.length != 0) {
if (daysPassed == 1) {
const todayAt0Utc = today * 86400000;
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
const darvoOid =
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
const deal = await DailyDeal.findById(darvoOid);
if (deal) {
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
} else {
inventory.UsedDailyDeals = [];
}
} else {
inventory.UsedDailyDeals = [];
}
}
}
// TODO: Setup CalendarProgress as part of 1999 mission completion?
const previousYearIteration = inventory.CalendarProgress?.Iteration;
// We need to do the following to ensure the in-game calendar does not break:
getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364)
checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424)
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress!.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail);
kalymos = false;
break;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem");
}
} }
cleanupInventory(inventory); cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000); inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
//await inventory.save(); //await inventory.save();
} }
@ -134,13 +214,21 @@ export const getInventoryResponse = async (
xpBasedLevelCapDisabled: boolean, xpBasedLevelCapDisabled: boolean,
buildLabel: string | undefined buildLabel: string | undefined
): Promise<IInventoryClient> => { ): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships] = await Promise.all([ const [inventoryWithLoadOutPresets, ships, latestMessage] = await Promise.all([
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
Ship.find({ ShipOwnerId: inventory.accountOwnerId }) Ship.find({ ShipOwnerId: inventory.accountOwnerId }),
Inbox.findOne({ ownerId: inventory.accountOwnerId }, "_id").sort({ date: -1 })
]); ]);
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>(); const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>()); inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
if (latestMessage) {
inventoryResponse.Mailbox = {
LastInboxId: toOid(latestMessage._id)
};
}
if (config.infiniteCredits) { if (config.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
} }
@ -167,18 +255,6 @@ export const getInventoryResponse = async (
} }
} }
if (config.unlockAllMissions) {
inventoryResponse.Missions = [];
for (const tag of Object.keys(ExportRegions)) {
inventoryResponse.Missions.push({
Completes: 1,
Tier: 1,
Tag: tag
});
}
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
}
if (config.unlockAllShipDecorations) { if (config.unlockAllShipDecorations) {
inventoryResponse.ShipDecorations = []; inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) { for (const [uniqueName, item] of Object.entries(ExportResources)) {
@ -300,9 +376,6 @@ export const getInventoryResponse = async (
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
} }
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
inventoryResponse.LastInventorySync = undefined;
// Set 2FA enabled so trading post can be used // Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true; inventoryResponse.HWIDProtectEnabled = true;
@ -339,14 +412,41 @@ export const getInventoryResponse = async (
} }
} }
if (config.unlockAllProfitTakerStages) {
inventoryResponse.CompletedJobChains ??= [];
const EudicoHeists = inventoryResponse.CompletedJobChains.find(x => x.LocationTag == "EudicoHeists");
if (EudicoHeists) {
EudicoHeists.Jobs = allEudicoHeistJobs;
} else {
inventoryResponse.CompletedJobChains.push({
LocationTag: "EudicoHeists",
Jobs: allEudicoHeistJobs
});
}
}
if (config.unlockAllSimarisResearchEntries) {
inventoryResponse.LibraryPersonalTarget = undefined;
inventoryResponse.LibraryPersonalProgress = [
"/Lotus/Types/Game/Library/Targets/Research1Target",
"/Lotus/Types/Game/Library/Targets/Research2Target",
"/Lotus/Types/Game/Library/Targets/Research3Target",
"/Lotus/Types/Game/Library/Targets/Research4Target",
"/Lotus/Types/Game/Library/Targets/Research5Target",
"/Lotus/Types/Game/Library/Targets/Research6Target",
"/Lotus/Types/Game/Library/Targets/Research7Target"
].map(type => ({ TargetType: type, Scans: 10, Completed: true }));
}
return inventoryResponse; return inventoryResponse;
}; };
const addString = (arr: string[], str: string): void => { const allEudicoHeistJobs = [
if (arr.indexOf(str) == -1) { "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne",
arr.push(str); "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyTwo",
} "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree",
}; "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyFour"
];
const getExpRequiredForMr = (rank: number): number => { const getExpRequiredForMr = (rank: number): number => {
if (rank <= 30) { if (rank <= 30) {

View File

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

View File

@ -4,16 +4,16 @@ import { config } from "@/src/services/configService";
import { buildConfig } from "@/src/services/buildConfigService"; import { buildConfig } from "@/src/services/buildConfigService";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/helpers/inventoryHelpers"; import { version_compare } from "@/src/helpers/inventoryHelpers";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const loginController: RequestHandler = async (request, response) => { export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
const account = await Account.findOne({ email: loginRequest.email }); const account = await Account.findOne({ email: loginRequest.email });
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER);
const buildLabel: string = const buildLabel: string =
typeof request.query.buildLabel == "string" typeof request.query.buildLabel == "string"
@ -42,26 +42,14 @@ export const loginController: RequestHandler = async (request, response) => {
loginRequest.ClientType == "webui-register") loginRequest.ClientType == "webui-register")
) { ) {
try { try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); const name = await getUsernameFromEmail(loginRequest.email);
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
if (await isNameTaken(name)) {
let suffix = 0;
do {
++suffix;
name = nameFromEmail + suffix;
} while (await isNameTaken(name));
}
const newAccount = await createAccount({ const newAccount = await createAccount({
email: loginRequest.email, email: loginRequest.email,
password: loginRequest.password, password: loginRequest.password,
DisplayName: name, DisplayName: name,
CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", CountryCode: loginRequest.lang?.toUpperCase() ?? "EN",
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true, Nonce: createNonce(),
ForceLogoutVersion: 0,
ConsentNeeded: false,
TrackedSettings: [],
Nonce: nonce,
BuildLabel: buildLabel, BuildLabel: buildLabel,
LastLogin: new Date() LastLogin: new Date()
}); });
@ -80,38 +68,29 @@ export const loginController: RequestHandler = async (request, response) => {
return; return;
} }
if (loginRequest.ClientType == "webui-register") {
response.status(400).json({ error: "account already exists" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) { if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" }); response.status(400).json({ error: "incorrect login data" });
return; return;
} }
if (loginRequest.ClientType == "webui") { if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
if (!account.Nonce) { // U17 seems to handle "nonce still set" like a login failure.
account.ClientType = "webui"; if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
account.Nonce = nonce; response.status(400).send({ error: "nonce still set" });
return;
} }
} else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
// U17 seems to handle "nonce still set" like a login failure.
if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) {
response.status(400).send({ error: "nonce still set" });
return;
}
}
account.ClientType = loginRequest.ClientType;
account.Nonce = nonce;
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
account.BuildLabel = buildLabel;
account.LastLogin = new Date();
} }
account.ClientType = loginRequest.ClientType;
account.Nonce = createNonce();
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN";
account.BuildLabel = buildLabel;
account.LastLogin = new Date();
await account.save(); await account.save();
// Tell WebUI its nonce has been invalidated
sendWsBroadcastTo(account._id.toString(), { logged_out: true });
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
}; };

View File

@ -8,6 +8,8 @@ import {
setAccountGotLoginRewardToday setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService"; } from "@/src/services/loginRewardService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const loginRewardsController: RequestHandler = async (req, res) => { export const loginRewardsController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
@ -15,7 +17,7 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0; const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50; const nextMilestoneDay = account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50;
if (today == account.LastLoginRewardDate) { if (today == account.LastLoginRewardDate || config.disableDailyTribute) {
res.json({ res.json({
DailyTributeInfo: { DailyTributeInfo: {
IsMilestoneDay: isMilestoneDay, IsMilestoneDay: isMilestoneDay,
@ -46,10 +48,10 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
response.DailyTributeInfo.HasChosenReward = true; response.DailyTributeInfo.HasChosenReward = true;
response.DailyTributeInfo.ChosenReward = randomRewards[0]; response.DailyTributeInfo.ChosenReward = randomRewards[0];
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]); response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
await inventory.save();
setAccountGotLoginRewardToday(account); setAccountGotLoginRewardToday(account);
await account.save(); await Promise.all([inventory.save(), account.save()]);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
} }
res.json(response); res.json(response);
}; };

View File

@ -6,6 +6,7 @@ import {
} from "@/src/services/loginRewardService"; } from "@/src/services/loginRewardService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -34,11 +35,10 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!; chosenReward = randomRewards.find(x => x.StoreItemType == body.ChosenReward)!;
inventoryChanges = await claimLoginReward(inventory, chosenReward); inventoryChanges = await claimLoginReward(inventory, chosenReward);
} }
await inventory.save();
setAccountGotLoginRewardToday(account); setAccountGotLoginRewardToday(account);
await account.save(); await Promise.all([inventory.save(), account.save()]);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
res.json({ res.json({
DailyTributeInfo: { DailyTributeInfo: {
NewInventory: inventoryChanges, NewInventory: inventoryChanges,

View File

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

View File

@ -3,10 +3,12 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService"; import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { generateRewardSeed, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController"; import { getInventoryResponse } from "@/src/controllers/api/inventoryController";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes"; import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { generateRewardSeed } from "@/src/services/rngService";
/* /*
**** INPUT **** **** INPUT ****
@ -76,6 +78,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
InventoryJson: JSON.stringify(inventoryResponse), InventoryJson: JSON.stringify(inventoryResponse),
MissionRewards: [] MissionRewards: []
}); });
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
return; return;
} }
@ -86,7 +89,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
AffiliationMods, AffiliationMods,
SyndicateXPItemReward, SyndicateXPItemReward,
ConquestCompletedMissionsCount ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion); } = await addMissionRewards(account, inventory, missionReport, firstCompletion);
if (missionReport.EndOfMatchUpload) { if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed(); inventory.RewardSeed = generateRewardSeed();
@ -106,6 +109,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
AffiliationMods, AffiliationMods,
ConquestCompletedMissionsCount ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse); } satisfies IMissionInventoryUpdateResponse);
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
}; };
/* /*

View File

@ -15,10 +15,9 @@ import {
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getDefaultUpgrades } from "@/src/services/itemDataService"; import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService"; import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus"; import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { IEquipmentDatabase, Status } from "@/src/types/equipmentTypes";
interface IModularCraftRequest { interface IModularCraftRequest {
WeaponType: string; WeaponType: string;

View File

@ -3,7 +3,7 @@ import { ExportWeapons } from "warframe-public-export-plus";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { import {
addEquipment, addEquipment,
@ -17,6 +17,7 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => { export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {}; const partTypeToParts: Record<string, string[]> = {};

View File

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

View File

@ -1,27 +1,28 @@
import { version_compare } from "@/src/helpers/inventoryHelpers"; import { version_compare } from "@/src/helpers/inventoryHelpers";
import { import {
consumeModCharge, antivirusMods,
decodeNemesisGuess, decodeNemesisGuess,
encodeNemesisGuess, encodeNemesisGuess,
getInfNodes, getInfNodes,
getKnifeUpgrade, getKnifeUpgrade,
getNemesisManifest, getNemesisManifest,
getNemesisPasscode, getNemesisPasscode,
getNemesisPasscodeModTypes,
GUESS_CORRECT, GUESS_CORRECT,
GUESS_INCORRECT, GUESS_INCORRECT,
GUESS_NEUTRAL, GUESS_NEUTRAL,
GUESS_NONE, GUESS_NONE,
GUESS_WILDCARD, GUESS_WILDCARD,
IKnifeResponse IKnifeResponse,
parseUpgrade
} from "@/src/helpers/nemesisHelpers"; } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { addMods, freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { import {
IInnateDamageFingerprint, IInnateDamageFingerprint,
IInventoryClient, IInventoryClient,
@ -35,6 +36,7 @@ import {
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose";
export const nemesisController: RequestHandler = async (req, res) => { export const nemesisController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
@ -134,34 +136,38 @@ export const nemesisController: RequestHandler = async (req, res) => {
for (const upgrade of body.knife!.AttachedUpgrades) { for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) { switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod": case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod": case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10; antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break; break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15; antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades); consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break; break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
} }
} }
inventory.Nemesis!.HenchmenKilled += antivirusGain; inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
// Weaken nemesis now.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, antivirusMods[passcode]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
} }
if (inventory.Nemesis!.HenchmenKilled >= 100) { if (inventory.Nemesis!.HenchmenKilled < 100) {
inventory.Nemesis!.HenchmenKilled = 100; inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
} }
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0);
await inventory.save(); await inventory.save();
res.json(response); res.json(response);
@ -198,16 +204,40 @@ export const nemesisController: RequestHandler = async (req, res) => {
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT; guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess); inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
// Increase rank if incorrect const response: INemesisRequiemResponse = {};
let RankIncrease: number | undefined; if (correct) {
if (!correct) { if (body.position == 2) {
RankIncrease = 1; // That was all 3 guesses correct, nemesis is now weakened.
inventory.Nemesis!.InfNodes = [
{
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
// Subtract a charge from all requiem mods installed on parazon
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(
inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid
);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
for (let i = 3; i != 6; ++i) {
//logger.debug(`subtracting a charge from ${dataknifeUpgrades[i]}`);
const upgrade = parseUpgrade(inventory, dataknifeUpgrades[i]);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
}
} else {
// Guess was incorrect, increase rank
response.RankIncrease = 1;
const manifest = getNemesisManifest(inventory.Nemesis!.manifest); const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1); inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank); inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
} }
await inventory.save(); await inventory.save();
res.json({ RankIncrease }); res.json(response);
} }
} else if ((req.query.mode as string) == "rs") { } else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body // report spawn; POST but no application data in body
@ -277,36 +307,15 @@ export const nemesisController: RequestHandler = async (req, res) => {
target: inventory.toJSON().Nemesis target: inventory.toJSON().Nemesis
}); });
} else if ((req.query.mode as string) == "w") { } else if ((req.query.mode as string) == "w") {
const inventory = await getInventory( const inventory = await getInventory(account._id.toString(), "Nemesis");
account._id.toString(),
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body)); //const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [ // As of 38.6.0, this request is no longer sent, instead mode=r already weakens the nemesis if appropriate.
{ // We always weaken the nemesis in mode=r so simply giving the client back the nemesis.
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const response: IKnifeResponse & { target: INemesisClient } = { const response: INemesisWeakenResponse = {
target: inventory.toJSON<IInventoryClient>().Nemesis! target: inventory.toJSON<IInventoryClient>().Nemesis!
}; };
// Consume charge of the correct requiem mod(s)
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
for (const modType of modTypes) {
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
await inventory.save();
res.json(response); res.json(response);
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
@ -362,11 +371,19 @@ interface INemesisRequiemRequest {
knife?: IKnife; knife?: IKnife;
} }
interface INemesisRequiemResponse extends IKnifeResponse {
RankIncrease?: number;
}
// interface INemesisWeakenRequest { // interface INemesisWeakenRequest {
// target: INemesisClient; // target: INemesisClient;
// knife: IKnife; // knife: IKnife;
// } // }
interface INemesisWeakenResponse extends IKnifeResponse {
target: INemesisClient;
}
interface IKnife { interface IKnife {
Item: IEquipmentClient; Item: IEquipmentClient;
Skins: IWeaponSkinClient[]; Skins: IWeaponSkinClient[];
@ -375,3 +392,54 @@ interface IKnife {
AttachedUpgrades: IUpgradeClient[]; AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean; HiddenWhenHolstered: boolean;
} }
const consumeModCharge = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string },
dataknifeUpgrades: string[]
): void => {
response.UpgradeIds ??= [];
response.UpgradeTypes ??= [];
response.UpgradeFingerprints ??= [];
response.UpgradeNew ??= [];
response.HasKnife = true;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);
} else {
const id = new Types.ObjectId();
inventory.Upgrades.push({
_id: id,
ItemType: upgrade.ItemType,
UpgradeFingerprint: `{"lvl":1}`
});
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType);
if (dataknifeRawUpgradeIndex != -1) {
dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString();
} else {
logger.warn(`${upgrade.ItemType} not found in dataknife config`);
}
response.UpgradeIds.push(id.toString());
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push({ lvl: 1 });
response.UpgradeNew.push(true);
}
};

View File

@ -57,7 +57,15 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
component.DecoCapacity -= meta.capacityCost; component.DecoCapacity -= meta.capacityCost;
} }
} else { } else {
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0]; const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type);
if (!entry) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
const [itemType, meta] = entry;
if (meta.dojoCapacityCost === undefined) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
component.DecoCapacity -= meta.dojoCapacityCost;
if (deco.Sockets !== undefined) { if (deco.Sockets !== undefined) {
guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -= guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -=
1; 1;
@ -71,7 +79,13 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
if (meta) { if (meta) {
processDojoBuildMaterialsGathered(guild, meta); processDojoBuildMaterialsGathered(guild, meta);
} }
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) { } else if (
deco.Type.startsWith("/Lotus/Objects/Tenno/Dojo/NpcPlaceables/") ||
(guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems)
) {
if (!guild.VaultRegularCredits || !guild.VaultMiscItems) {
throw new Error(`dojo visitor placed without anything in vault?!`);
}
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) { if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let enoughMiscItems = true; let enoughMiscItems = true;
for (const ingredient of meta.ingredients) { for (const ingredient of meta.ingredients) {

View File

@ -1,25 +1,39 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { addConsumables, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes"; import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const playerSkillsController: RequestHandler = async (req, res) => { export const playerSkillsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PlayerSkills"); const inventory = await getInventory(accountId, "PlayerSkills Consumables");
const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body)); const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]; const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000; const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost; inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++; inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
await inventory.save();
const inventoryChanges: IInventoryChanges = {};
if (request.Skill == "LPS_COMMAND" && inventory.PlayerSkills.LPS_COMMAND == 9) {
const consumablesChanges = [
{
ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall",
ItemCount: 1
}
];
addConsumables(inventory, consumablesChanges);
inventoryChanges.Consumables = consumablesChanges;
}
await inventory.save();
res.json({ res.json({
Pool: request.Pool, Pool: request.Pool,
PoolInc: -cost, PoolInc: -cost,
Skill: request.Skill, Skill: request.Skill,
Rank: oldRank + 1 Rank: oldRank + 1,
InventoryChanges: inventoryChanges
}); });
}; };

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { Status } from "@/src/types/equipmentTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const retrievePetFromStasisController: RequestHandler = async (req, res) => { export const retrievePetFromStasisController: RequestHandler = async (req, res) => {

View File

@ -24,7 +24,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
inventory.DialogueHistory.Dialogues ??= []; inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName); const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank; dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry; dialogue.Chemistry += request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues; dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) { for (const bool of request.Booleans) {
dialogue.Booleans.push(bool); dialogue.Booleans.push(bool);

View File

@ -2,7 +2,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ISettings } from "../../types/inventoryTypes/inventoryTypes"; import { ISettings } from "@/src/types/inventoryTypes/inventoryTypes";
interface ISaveSettingsRequest { interface ISaveSettingsRequest {
Settings: ISettings; Settings: ISettings;

View File

@ -15,9 +15,11 @@ import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const sellController: RequestHandler = async (req, res) => { export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest; const payload = JSON.parse(String(req.body)) as ISellRequest;
//console.log(JSON.stringify(payload, null, 2));
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const requiredFields = new Set<keyof TInventoryDatabaseDocument>(); const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
if (payload.SellCurrency == "SC_RegularCredits") { if (payload.SellCurrency == "SC_RegularCredits") {
@ -57,6 +59,9 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.Hoverboards) { if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS); requiredFields.add(InventorySlot.SPACESUITS);
} }
if (payload.Items.CrewMembers) {
requiredFields.add(InventorySlot.CREWMEMBERS);
}
if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) { if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS); requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
requiredFields.add("CrewShipRawSalvage"); requiredFields.add("CrewShipRawSalvage");
@ -180,6 +185,17 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.Drones.pull({ _id: sellItem.String }); inventory.Drones.pull({ _id: sellItem.String });
}); });
} }
if (payload.Items.KubrowPetPrints) {
payload.Items.KubrowPetPrints.forEach(sellItem => {
inventory.KubrowPetPrints.pull({ _id: sellItem.String });
});
}
if (payload.Items.CrewMembers) {
payload.Items.CrewMembers.forEach(sellItem => {
inventory.CrewMembers.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.CREWMEMBERS);
});
}
if (payload.Items.CrewShipWeapons) { if (payload.Items.CrewShipWeapons) {
payload.Items.CrewShipWeapons.forEach(sellItem => { payload.Items.CrewShipWeapons.forEach(sellItem => {
if (sellItem.String[0] == "/") { if (sellItem.String[0] == "/") {
@ -279,6 +295,7 @@ export const sellController: RequestHandler = async (req, res) => {
res.json({ res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
}); });
sendWsBroadcastTo(accountId, { update_inventory: true });
}; };
interface ISellRequest { interface ISellRequest {
@ -301,6 +318,8 @@ interface ISellRequest {
OperatorAmps?: ISellItem[]; OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[]; Hoverboards?: ISellItem[];
Drones?: ISellItem[]; Drones?: ISellItem[];
KubrowPetPrints?: ISellItem[];
CrewMembers?: ISellItem[];
CrewShipWeapons?: ISellItem[]; CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[]; CrewShipWeaponSkins?: ISellItem[];
}; };

View File

@ -1,7 +1,7 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { TBootLocation } from "@/src/types/shipTypes"; import { TBootLocation } from "@/src/types/personalRoomsTypes";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
export const setBootLocationController: RequestHandler = async (req, res) => { export const setBootLocationController: RequestHandler = async (req, res) => {

View File

@ -1,5 +1,5 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/personalRoomsTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService"; import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";

View File

@ -1,6 +1,6 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { setShipCustomizations } from "@/src/services/shipCustomizationsService"; import { setShipCustomizations } from "@/src/services/shipCustomizationsService";
import { ISetShipCustomizationsRequest } from "@/src/types/shipTypes"; import { ISetShipCustomizationsRequest } from "@/src/types/personalRoomsTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";

View File

@ -3,7 +3,7 @@ import { RequestHandler } from "express";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes"; import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/personalRoomsTypes";
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => { export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);

View File

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

View File

@ -1,5 +1,5 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IShipDecorationsRequest } from "@/src/types/shipTypes"; import { IShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService"; import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService";

View File

@ -45,9 +45,9 @@ export const startRecipeController: RequestHandler = async (req, res) => {
for (let i = 0; i != recipe.ingredients.length; ++i) { for (let i = 0; i != recipe.ingredients.length; ++i) {
if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") {
if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") {
const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); const index = inventory.KubrowPetEggs.findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (index != -1) { if (index != -1) {
inventory.KubrowPetEggs!.splice(index, 1); inventory.KubrowPetEggs.splice(index, 1);
} }
} else { } else {
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory; const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
@ -72,6 +72,10 @@ export const startRecipeController: RequestHandler = async (req, res) => {
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") { if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType); inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId)); pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
} else if (recipe.secretIngredientAction == "SIA_DISTILL_PRINT") {
pr.KubrowPet = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length]);
const pet = inventory.KubrowPets.id(pr.KubrowPet)!;
pet.Details!.PrintsRemaining -= 1;
} else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { } else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const spectreLoadout: ISpectreLoadout = { const spectreLoadout: ISpectreLoadout = {
ItemType: recipe.resultType, ItemType: recipe.resultType,

View File

@ -31,13 +31,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
AffiliationTag: data.AffiliationTag, AffiliationTag: data.AffiliationTag,
InventoryChanges: {}, InventoryChanges: {},
Level: data.SacrificeLevel, Level: data.SacrificeLevel,
LevelIncrease: levelIncrease, LevelIncrease: data.SacrificeLevel < 0 ? 1 : levelIncrease,
NewEpisodeReward: false NewEpisodeReward: false
}; };
// Process sacrifices and rewards for every level we're reaching // Process sacrifices and rewards for every level we're reaching
const manifest = ExportSyndicates[data.AffiliationTag]; const manifest = ExportSyndicates[data.AffiliationTag];
for (let level = oldLevel + levelIncrease; level <= data.SacrificeLevel; ++level) { for (let level = oldLevel + Math.min(levelIncrease, 1); level <= data.SacrificeLevel; ++level) {
let sacrifice: ISyndicateSacrifice | undefined; let sacrifice: ISyndicateSacrifice | undefined;
if (level == 0) { if (level == 0) {
sacrifice = manifest.initiationSacrifice; sacrifice = manifest.initiationSacrifice;
@ -94,7 +94,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
} }
// Commit // Commit
syndicate.Title = data.SacrificeLevel; syndicate.Title = data.SacrificeLevel < 0 ? data.SacrificeLevel + 1 : data.SacrificeLevel;
await inventory.save(); await inventory.save();
response.json(res); response.json(res);

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { addCalendarProgress, addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { IAffiliationMods } from "@/src/types/purchaseTypes"; import { IAffiliationMods } from "@/src/types/purchaseTypes";
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { logger } from "@/src/utils/logger";
export const updateChallengeProgressController: RequestHandler = async (req, res) => { export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body)); const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
@ -11,7 +13,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
const inventory = await getInventory( const inventory = await getInventory(
account._id.toString(), account._id.toString(),
"ChallengeProgress SeasonChallengeHistory Affiliations" "ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations CalendarProgress"
); );
let affiliationMods: IAffiliationMods[] = []; let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) { if (challenges.ChallengeProgress) {
@ -22,15 +24,39 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
challenges.SeasonChallengeCompletions challenges.SeasonChallengeCompletions
); );
} }
if (challenges.SeasonChallengeHistory) { for (const [key, value] of getEntriesUnsafe(challenges)) {
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { if (value === undefined) {
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge); logger.error(`Challenge progress update key ${key} has no value`);
if (itemIndex !== -1) { continue;
inventory.SeasonChallengeHistory[itemIndex].id = id; }
} else { switch (key) {
inventory.SeasonChallengeHistory.push({ challenge, id }); case "ChallengesFixVersion":
} inventory.ChallengesFixVersion = value;
}); break;
case "SeasonChallengeHistory":
value.forEach(({ challenge, id }) => {
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
if (itemIndex !== -1) {
inventory.SeasonChallengeHistory[itemIndex].id = id;
} else {
inventory.SeasonChallengeHistory.push({ challenge, id });
}
});
break;
case "CalendarProgress":
addCalendarProgress(inventory, value);
break;
case "ChallengeProgress":
case "SeasonChallengeCompletions":
case "ChallengePTS":
case "crossPlaySetting":
break;
default:
logger.warn(`unknown challenge progress entry`, { key, value });
}
} }
await inventory.save(); await inventory.save();
@ -40,7 +66,11 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
}; };
interface IUpdateChallengeProgressRequest { interface IUpdateChallengeProgressRequest {
ChallengePTS?: number;
ChallengesFixVersion?: number;
ChallengeProgress?: IChallengeProgress[]; ChallengeProgress?: IChallengeProgress[];
SeasonChallengeHistory?: ISeasonChallenge[]; SeasonChallengeHistory?: ISeasonChallenge[];
SeasonChallengeCompletions?: ISeasonChallenge[]; SeasonChallengeCompletions?: ISeasonChallenge[];
CalendarProgress?: { challenge: string }[];
crossPlaySetting?: string;
} }

View File

@ -1,11 +1,6 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { IUpgradesRequest } from "@/src/types/requestTypes"; import { IUpgradesRequest } from "@/src/types/requestTypes";
import { import { ArtifactPolarity, IAbilityOverride } from "@/src/types/inventoryTypes/commonInventoryTypes";
ArtifactPolarity,
IEquipmentDatabase,
EquipmentFeatures,
IAbilityOverride
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IInventoryClient, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
@ -13,6 +8,8 @@ import { getRecipeByResult } from "@/src/services/itemDataService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService"; import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { EquipmentFeatures, IEquipmentDatabase } from "@/src/types/equipmentTypes";
export const upgradesController: RequestHandler = async (req, res) => { export const upgradesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -120,6 +117,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue); setSlotPolarity(item, operation.PolarizeSlot, operation.PolarizeValue);
item.Polarized ??= 0; item.Polarized ??= 0;
item.Polarized += 1; item.Polarized += 1;
sendWsBroadcastTo(accountId, { update_inventory: true }); // webui may need to to re-add "max rank" button
break; break;
} }
case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": { case "/Lotus/Types/Items/MiscItems/ModSlotUnlocker": {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportMisc } from "warframe-public-export-plus"; import { ExportMisc } from "warframe-public-export-plus";

View File

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

View File

@ -0,0 +1,46 @@
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { saveConfig } from "@/src/services/configWriterService";
import { sendWsBroadcastExcept } from "@/src/services/wsService";
export const getConfigController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
const responseData: Record<string, boolean | string | number | null> = {};
for (const id of req.body as string[]) {
const [obj, idx] = configIdToIndexable(id);
responseData[id] = obj[idx] ?? null;
}
res.json(responseData);
} else {
res.status(401).end();
}
};
export const setConfigController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
for (const [id, value] of Object.entries(req.body as Record<string, boolean | string | number>)) {
const [obj, idx] = configIdToIndexable(id);
obj[idx] = value;
}
sendWsBroadcastExcept(parseInt(String(req.query.wsid)), { config_reloaded: true });
await saveConfig();
res.end();
} else {
res.status(401).end();
}
};
const configIdToIndexable = (id: string): [Record<string, boolean | string | number | undefined>, string] => {
let obj = config as unknown as Record<string, never>;
const arr = id.split(".");
while (arr.length > 1) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
obj[arr[0]] ??= {} as never;
obj = obj[arr[0]];
arr.splice(0, 1);
}
return [obj, arr[0]];
};

View File

@ -1,14 +0,0 @@
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
const getConfigDataController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
res.json(config);
} else {
res.status(401).end();
}
};
export { getConfigDataController };

View File

@ -20,8 +20,8 @@ import {
ExportWeapons, ExportWeapons,
TRelicQuality TRelicQuality
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
import varzia from "@/static/fixed_responses/worldState/varzia.json";
interface ListedItem { interface ListedItem {
uniqueName: string; uniqueName: string;
@ -36,7 +36,6 @@ interface ListedItem {
} }
interface ItemLists { interface ItemLists {
archonCrystalUpgrades: Record<string, string>;
uniqueLevelCaps: Record<string, number>; uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[]; Suits: ListedItem[];
LongGuns: ListedItem[]; LongGuns: ListedItem[];
@ -57,6 +56,8 @@ interface ItemLists {
EvolutionProgress: ListedItem[]; EvolutionProgress: ListedItem[];
mods: ListedItem[]; mods: ListedItem[];
Boosters: ListedItem[]; Boosters: ListedItem[];
VarziaOffers: ListedItem[];
//circuitGameModes: ListedItem[];
} }
const relicQualitySuffixes: Record<TRelicQuality, string> = { const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -66,10 +67,13 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
VPQ_PLATINUM: " [Exceptional]" VPQ_PLATINUM: " [Exceptional]"
}; };
/*const toTitleCase = (str: string): string => {
return str.replace(/[^\s-]+/g, word => word.charAt(0).toUpperCase() + word.substr(1).toLowerCase());
};*/
const getItemListsController: RequestHandler = (req, response) => { const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = { const res: ItemLists = {
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps, uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [], Suits: [],
LongGuns: [], LongGuns: [],
@ -89,7 +93,38 @@ const getItemListsController: RequestHandler = (req, response) => {
KubrowPets: [], KubrowPets: [],
EvolutionProgress: [], EvolutionProgress: [],
mods: [], mods: [],
Boosters: [] Boosters: [],
VarziaOffers: []
/*circuitGameModes: [
{
uniqueName: "Survival",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Survival", lang))
},
{
uniqueName: "VoidFlood",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Corruption", lang))
},
{
uniqueName: "Excavation",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Excavation", lang))
},
{
uniqueName: "Defense",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Defense", lang))
},
{
uniqueName: "Exterminate",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Exterminate", lang))
},
{
uniqueName: "Assassination",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Assassination", lang))
},
{
uniqueName: "Alchemy",
name: toTitleCase(getString("/Lotus/Language/Missions/MissionName_Alchemy", lang))
}
]*/
}; };
for (const [uniqueName, item] of Object.entries(ExportWarframes)) { for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({ res[item.productCategory].push({
@ -306,6 +341,13 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
for (const item of Object.values(varzia.primeDualPacks)) {
res.VarziaOffers.push({
uniqueName: item.ItemType,
name: getString(getItemName(item.ItemType) || "", lang)
});
}
response.json(res); response.json(res);
}; };

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { saveConfig } from "@/src/services/configWatcherService"; import { saveConfig } from "@/src/services/configWriterService";
export const renameAccountController: RequestHandler = async (req, res) => { export const renameAccountController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);

View File

@ -23,9 +23,9 @@ export const setBoosterController: RequestHandler = async (req, res) => {
res.status(400).send("Invalid ItemType provided."); res.status(400).send("Invalid ItemType provided.");
return; return;
} }
const now = Math.floor(Date.now() / 1000); const now = Math.trunc(Date.now() / 1000);
for (const { ItemType, ExpiryDate } of requests) { for (const { ItemType, ExpiryDate } of requests) {
if (ExpiryDate < now) { if (ExpiryDate <= now) {
// remove expired boosters // remove expired boosters
const index = boosters.findIndex(item => item.ItemType === ItemType); const index = boosters.findIndex(item => item.ItemType === ItemType);
if (index !== -1) { if (index !== -1) {

View File

@ -1,15 +0,0 @@
import { RequestHandler } from "express";
import { updateConfig } from "@/src/services/configWatcherService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
const updateConfigDataController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
if (isAdministrator(account)) {
await updateConfig(String(req.body));
res.end();
} else {
res.status(401).end();
}
};
export { updateConfigDataController };

View File

@ -0,0 +1,39 @@
import { getInventory } from "@/src/services/inventoryService";
import { WeaponTypeInternal } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const updateFingerprintController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IUpdateFingerPrintRequest;
const inventory = await getInventory(accountId, request.category);
const item = inventory[request.category].id(request.oid);
if (item) {
if (request.action == "set" && request.upgradeFingerprint.buffs[0].Tag) {
const newUpgradeFingerprint = request.upgradeFingerprint;
if (!newUpgradeFingerprint.compact) newUpgradeFingerprint.compact = item.ItemType;
item.UpgradeType = request.upgradeType;
item.UpgradeFingerprint = JSON.stringify(newUpgradeFingerprint);
} else if (request.action == "remove") {
item.UpgradeFingerprint = undefined;
item.UpgradeType = undefined;
}
await inventory.save();
}
res.end();
};
interface IUpdateFingerPrintRequest {
category: WeaponTypeInternal;
oid: string;
action: "set" | "remove";
upgradeType: string;
upgradeFingerprint: {
compact?: string;
buffs: {
Tag: string;
Value: number;
}[];
};
}

View File

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

View File

@ -6,13 +6,11 @@ import { Account } from "@/src/models/loginModel";
import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel"; import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; import { allDailyAffiliationKeys } from "@/src/services/inventoryService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { import {
IAffiliation, IAffiliation,
IAlignment, IAlignment,
IChallengeProgress, IChallengeProgress,
IDailyAffiliations, IDailyAffiliations,
ILoadoutConfigClient,
IMission, IMission,
IPlayerSkills, IPlayerSkills,
ITypeXPItem ITypeXPItem
@ -23,6 +21,8 @@ import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
import { IStatsClient } from "@/src/types/statTypes"; import { IStatsClient } from "@/src/types/statTypes";
import { toStoreItem } from "@/src/services/itemDataService"; import { toStoreItem } from "@/src/services/itemDataService";
import { FlattenMaps } from "mongoose"; import { FlattenMaps } from "mongoose";
import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { ILoadoutConfigClient } from "@/src/types/saveLoadoutTypes";
const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IProfileViewingData | undefined> => { const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IProfileViewingData | undefined> => {
const account = await Account.findById(playerId, "DisplayName"); const account = await Account.findById(playerId, "DisplayName");

View File

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

View File

@ -0,0 +1,23 @@
interface IArguments {
configPath?: string;
dev?: boolean;
secret?: string;
}
export const args: IArguments = {};
for (let i = 2; i < process.argv.length; ) {
switch (process.argv[i++]) {
case "--configPath":
args.configPath = process.argv[i++];
break;
case "--dev":
args.dev = true;
break;
case "--secret":
args.secret = process.argv[i++];
break;
}
}

View File

@ -1,7 +1,7 @@
import { IAccountCreation } from "@/src/types/customTypes"; import { IAccountCreation } from "@/src/types/customTypes";
import { IDatabaseAccountRequiredFields } from "@/src/types/loginTypes"; import { IDatabaseAccountRequiredFields } from "@/src/types/loginTypes";
import crypto from "crypto"; import crypto from "crypto";
import { isString, parseEmail, parseString } from "../general"; import { isString, parseEmail, parseString } from "@/src/helpers/general";
const getWhirlpoolHash = (rawPassword: string): string => { const getWhirlpoolHash = (rawPassword: string): string => {
const whirlpool = crypto.createHash("whirlpool"); const whirlpool = crypto.createHash("whirlpool");

View File

@ -9,11 +9,11 @@ export const isEmptyObject = (obj: object): boolean => {
}; };
*/ */
const isString = (text: unknown): text is string => { export const isString = (text: unknown): text is string => {
return typeof text === "string" || text instanceof String; return typeof text === "string" || text instanceof String;
}; };
const parseString = (data: unknown): string => { export const parseString = (data: unknown): string => {
if (!isString(data)) { if (!isString(data)) {
throw new Error("data is not a string"); throw new Error("data is not a string");
} }
@ -21,11 +21,11 @@ const parseString = (data: unknown): string => {
return data; return data;
}; };
const isNumber = (number: unknown): number is number => { export const isNumber = (number: unknown): number is number => {
return typeof number === "number" && !isNaN(number); return typeof number === "number" && !isNaN(number);
}; };
const parseNumber = (data: unknown): number => { export const parseNumber = (data: unknown): number => {
if (!isNumber(data)) { if (!isNumber(data)) {
throw new Error("data is not a number"); throw new Error("data is not a number");
} }
@ -33,11 +33,11 @@ const parseNumber = (data: unknown): number => {
return Number(data); return Number(data);
}; };
const isDate = (date: string): boolean => { export const isDate = (date: string): boolean => {
return Date.parse(date) != 0; return Date.parse(date) != 0;
}; };
const parseDateNumber = (date: unknown): string => { export const parseDateNumber = (date: unknown): string => {
if (!isString(date) || !isDate(date)) { if (!isString(date) || !isDate(date)) {
throw new Error("date could not be parsed"); throw new Error("date could not be parsed");
} }
@ -45,18 +45,18 @@ const parseDateNumber = (date: unknown): string => {
return date; return date;
}; };
const parseEmail = (email: unknown): string => { export const parseEmail = (email: unknown): string => {
if (!isString(email)) { if (!isString(email)) {
throw new Error("incorrect email"); throw new Error("incorrect email");
} }
return email; return email;
}; };
const isBoolean = (booleanCandidate: unknown): booleanCandidate is boolean => { export const isBoolean = (booleanCandidate: unknown): booleanCandidate is boolean => {
return typeof booleanCandidate === "boolean"; return typeof booleanCandidate === "boolean";
}; };
const parseBoolean = (booleanCandidate: unknown): boolean => { export const parseBoolean = (booleanCandidate: unknown): boolean => {
if (!isBoolean(booleanCandidate)) { if (!isBoolean(booleanCandidate)) {
throw new Error("argument was not a boolean"); throw new Error("argument was not a boolean");
} }
@ -70,5 +70,3 @@ export const isObject = (objectCandidate: unknown): objectCandidate is Record<st
!Array.isArray(objectCandidate) !Array.isArray(objectCandidate)
); );
}; };
export { isString, isNumber, parseString, parseNumber, parseDateNumber, parseBoolean, parseEmail };

View File

@ -51,6 +51,11 @@ export const fromMongoDate = (date: IMongoDate): Date => {
return new Date(parseInt(date.$date.$numberLong)); return new Date(parseInt(date.$date.$numberLong));
}; };
export type TTraitsPool = Record<
"Colors" | "EyeColors" | "FurPatterns" | "BodyTypes" | "Heads" | "Tails",
{ type: string; rarity: TRarity }[]
>;
export const kubrowWeights: Record<TRarity, number> = { export const kubrowWeights: Record<TRarity, number> = {
COMMON: 6, COMMON: 6,
UNCOMMON: 4, UNCOMMON: 4,
@ -65,126 +70,126 @@ export const kubrowFurPatternsWeights: Record<TRarity, number> = {
LEGENDARY: 1 LEGENDARY: 1
}; };
export const catbrowDetails = { export const catbrowDetails: TTraitsPool = {
Colors: [ Colors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" }
], ],
EyeColors: [ EyeColors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" }
], ],
FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" as TRarity }], FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" }],
BodyTypes: [ BodyTypes: [
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" }
], ],
Heads: [ Heads: [
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" }
], ],
Tails: [ Tails: [
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" }
] ]
}; };
export const kubrowDetails = { export const kubrowDetails: TTraitsPool = {
Colors: [ Colors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" }
], ],
EyeColors: [ EyeColors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" }
], ],
FurPatterns: [ FurPatterns: [
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" }
], ],
BodyTypes: [ BodyTypes: [
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" as TRarity }, { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" as TRarity } { type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" }
], ],
Heads: [], Heads: [],

View File

@ -1,4 +1,4 @@
import { TEquipmentKey } from "../types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
export const modularWeaponTypes: Record<string, TEquipmentKey> = { export const modularWeaponTypes: Record<string, TEquipmentKey> = {
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns", "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns",

View File

@ -1,12 +1,9 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes";
import { getRewardAtPercentage, SRng } from "@/src/services/rngService"; import { generateRewardSeed, getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger"; import { IOid } from "@/src/types/commonTypes";
import { IOid } from "../types/commonTypes"; import { isArchwingMission } from "@/src/services/worldStateService";
import { Types } from "mongoose";
import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission } from "../services/worldStateService";
type TInnateDamageTag = type TInnateDamageTag =
| "InnateElectricityDamage" | "InnateElectricityDamage"
@ -237,7 +234,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
return passcode; return passcode;
}; };
const requiemMods: readonly string[] = [ /*const requiemMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
@ -246,9 +243,9 @@ const requiemMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod" "/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod"
]; ];*/
const antivirusMods: readonly string[] = [ export const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod", "/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod", "/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod", "/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
@ -259,12 +256,12 @@ const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
]; ];
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => { /*export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => {
const passcode = getNemesisPasscode(nemesis); const passcode = getNemesisPasscode(nemesis);
return nemesis.Faction == "FC_INFESTATION" return nemesis.Faction == "FC_INFESTATION"
? passcode.map(i => antivirusMods[i]) ? passcode.map(i => antivirusMods[i])
: passcode.map(i => requiemMods[i]); : passcode.map(i => requiemMods[i]);
}; };*/
// Symbols; 0-7 are the normal requiem mods. // Symbols; 0-7 are the normal requiem mods.
export const GUESS_NONE = 8; export const GUESS_NONE = 8;
@ -343,54 +340,24 @@ export const getKnifeUpgrade = (
throw new Error(`${type} does not seem to be installed on parazon?!`); throw new Error(`${type} does not seem to be installed on parazon?!`);
}; };
export const consumeModCharge = ( export const parseUpgrade = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string }, str: string
dataknifeUpgrades: string[] ): { ItemId: IOid; ItemType: string } => {
): void => { if (str.length == 24) {
response.UpgradeIds ??= []; const upgrade = inventory.Upgrades.id(str);
response.UpgradeTypes ??= []; if (upgrade) {
response.UpgradeFingerprints ??= []; return {
response.UpgradeNew ??= []; ItemId: { $oid: str },
response.HasKnife = true; ItemType: upgrade.ItemType
};
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);
} else {
const id = new Types.ObjectId();
inventory.Upgrades.push({
_id: id,
ItemType: upgrade.ItemType,
UpgradeFingerprint: `{"lvl":1}`
});
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType);
if (dataknifeRawUpgradeIndex != -1) {
dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString();
} else {
logger.warn(`${upgrade.ItemType} not found in dataknife config`);
} }
throw new Error(`Could not resolve oid ${str}`);
response.UpgradeIds.push(id.toString()); } else {
response.UpgradeTypes.push(upgrade.ItemType); return {
response.UpgradeFingerprints.push({ lvl: 1 }); ItemId: { $oid: "000000000000000000000000" },
response.UpgradeNew.push(true); ItemType: str
};
} }
}; };

View File

@ -1,5 +1,4 @@
import path from "path"; import path from "path";
export const rootDir = path.join(__dirname, "../.."); export const rootDir = path.join(__dirname, "../..");
export const isDev = path.basename(rootDir) != "build"; export const repoDir = path.basename(rootDir) != "build" ? rootDir : path.join(rootDir, "..");
export const repoDir = isDev ? rootDir : path.join(rootDir, "..");

View File

@ -5,7 +5,8 @@ import { getRandomWeightedReward, IRngResult } from "@/src/services/rngService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { addMiscItems, combineInventoryChanges } from "@/src/services/inventoryService"; import { addMiscItems, combineInventoryChanges } from "@/src/services/inventoryService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "../types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService";
export const crackRelic = async ( export const crackRelic = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
@ -13,12 +14,25 @@ export const crackRelic = async (
inventoryChanges: IInventoryChanges = {} inventoryChanges: IInventoryChanges = {}
): Promise<IRngResult> => { ): Promise<IRngResult> => {
const relic = ExportRelics[participant.VoidProjection]; const relic = ExportRelics[participant.VoidProjection];
const weights = refinementToWeights[relic.quality]; let weights = refinementToWeights[relic.quality];
if (relic.quality == "VPQ_SILVER" && config.exceptionalRelicsAlwaysGiveBronzeReward) {
weights = { COMMON: 1, UNCOMMON: 0, RARE: 0, LEGENDARY: 0 };
} else if (relic.quality == "VPQ_GOLD" && config.flawlessRelicsAlwaysGiveSilverReward) {
weights = { COMMON: 0, UNCOMMON: 1, RARE: 0, LEGENDARY: 0 };
} else if (relic.quality == "VPQ_PLATINUM" && config.radiantRelicsAlwaysGiveGoldReward) {
weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
}
logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights); logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
const reward = getRandomWeightedReward( let reward = getRandomWeightedReward(
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights weights
)!; )!;
if (config.relicRewardItemCountMultiplier !== undefined && (config.relicRewardItemCountMultiplier ?? 1) != 1) {
reward = {
...reward,
itemCount: reward.itemCount * config.relicRewardItemCountMultiplier
};
}
logger.debug(`relic rolled`, reward); logger.debug(`relic rolled`, reward);
participant.Reward = reward.type; participant.Reward = reward.type;

View File

@ -1,5 +1,5 @@
import { IUpgrade } from "warframe-public-export-plus"; import { IUpgrade } from "warframe-public-export-plus";
import { getRandomElement, getRandomInt, getRandomReward } from "../services/rngService"; import { getRandomElement, getRandomInt, getRandomReward } from "@/src/services/rngService";
export type RivenFingerprint = IVeiledRivenFingerprint | IUnveiledRivenFingerprint; export type RivenFingerprint = IVeiledRivenFingerprint | IUnveiledRivenFingerprint;

View File

@ -54,3 +54,9 @@ export const regexEscape = (str: string): string => {
str = str.split("}").join("\\}"); str = str.split("}").join("\\}");
return str; return str;
}; };
export const addString = (arr: string[], str: string): void => {
if (arr.indexOf(str) == -1) {
arr.push(str);
}
};

View File

@ -10,3 +10,14 @@ export const getMaxStanding = (syndicate: ISyndicate, title: number): number =>
} }
return syndicate.titles.find(x => x.level == title)!.maxStanding; return syndicate.titles.find(x => x.level == title)!.maxStanding;
}; };
export const getMinStanding = (syndicate: ISyndicate, title: number): number => {
if (!syndicate.titles) {
// LibrarySyndicate
return 0;
}
if (title == 0) {
return syndicate.titles.find(x => x.level == -1)!.maxStanding;
}
return syndicate.titles.find(x => x.level == title)!.minStanding;
};

View File

@ -1,9 +1,14 @@
// First, init config. // First, init config.
import { config, loadConfig } from "@/src/services/configService"; import { config, configPath, loadConfig } from "@/src/services/configService";
import fs from "fs";
try { try {
loadConfig(); loadConfig();
} catch (e) { } catch (e) {
console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json."); if (fs.existsSync("config.json")) {
console.log("Failed to load " + configPath + ": " + (e as Error).message);
} else {
console.log("Failed to load " + configPath + ". You can copy config.json.example to create your config file.");
}
process.exit(1); process.exit(1);
} }
@ -14,9 +19,10 @@ logger.info("Starting up...");
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. // Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
import mongoose from "mongoose"; import mongoose from "mongoose";
import { JSONStringify } from "json-with-bigint"; import { JSONStringify } from "json-with-bigint";
import { startWebServer } from "./services/webService"; import { startWebServer } from "@/src/services/webService";
import { validateConfig } from "@/src/services/configWatcherService"; import { syncConfigWithDatabase, validateConfig } from "@/src/services/configWatcherService";
import { updateWorldStateCollections } from "@/src/services/worldStateService";
// Patch JSON.stringify to work flawlessly with Bigints. // Patch JSON.stringify to work flawlessly with Bigints.
JSON.stringify = JSONStringify; JSON.stringify = JSONStringify;
@ -27,7 +33,14 @@ mongoose
.connect(config.mongodbUrl) .connect(config.mongodbUrl)
.then(() => { .then(() => {
logger.info("Connected to MongoDB"); logger.info("Connected to MongoDB");
syncConfigWithDatabase();
startWebServer(); startWebServer();
void updateWorldStateCollections();
setInterval(() => {
void updateWorldStateCollections();
}, 60_000);
}) })
.catch(error => { .catch(error => {
if (error instanceof Error) { if (error instanceof Error) {

View File

@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { logger } from "../utils/logger"; import { logger } from "@/src/utils/logger";
export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => { export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => {
if (err.message == "Invalid accountId-nonce pair") { if (err.message == "Invalid accountId-nonce pair") {

25
src/models/commonModel.ts Normal file
View File

@ -0,0 +1,25 @@
import { Schema } from "mongoose";
import { IColor, IShipCustomization } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const colorSchema = new Schema<IColor>(
{
t0: Number,
t1: Number,
t2: Number,
t3: Number,
en: Number,
e1: Number,
m0: Number,
m1: Number
},
{ _id: false }
);
export const shipCustomizationSchema = new Schema<IShipCustomization>(
{
SkinFlavourItem: String,
Colors: colorSchema,
ShipAttachments: { HOOD_ORNAMENT: String }
},
{ _id: false }
);

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