This commit is contained in:
VampireKitten 2025-04-07 00:55:31 +02:00
commit 5aedb579aa
229 changed files with 15204 additions and 3425 deletions

19
.coderabbit.yaml Normal file
View File

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

View File

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

View File

@ -17,5 +17,5 @@ jobs:
node-version: ${{ matrix.version }} node-version: ${{ matrix.version }}
- run: npm ci - run: npm ci
- run: cp config.json.example config.json - run: cp config.json.example config.json
- run: npm run build - run: npm run verify
- run: npm run lint - run: npm run lint

View File

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

View File

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

5
.vscode/extensions.json vendored Normal file
View File

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

View File

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

View File

@ -2,7 +2,14 @@
More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY) More information for the moment here: [https://discord.gg/PNNZ3asUuY](https://discord.gg/PNNZ3asUuY)
## Project Status
This project is in active development at <https://onlyg.it/OpenWF/SpaceNinjaServer>.
To get an idea of what functionality you can expect to be missing [have a look through the issues](https://onlyg.it/OpenWF/SpaceNinjaServer/issues?q=&type=all&state=open&labels=-4%2C-10&milestone=0&assignee=0&poster=). However, many things have been implemented and *should* work as expected. Please open an issue for anything where that's not the case and/or the server is reporting errors.
## config.json ## config.json
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.

View File

@ -3,6 +3,7 @@
echo Updating SpaceNinjaServer... echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune git fetch --prune
git stash
git reset --hard origin/main git reset --hard origin/main
if exist static\data\0\ ( if exist static\data\0\ (
@ -13,12 +14,13 @@ if exist static\data\0\ (
) )
echo Updating dependencies... echo Updating dependencies...
call npm i call npm i --omit=dev
call npm run build call npm run build
call npm run start if %errorlevel% == 0 (
call npm run start
echo SpaceNinjaServer seems to have crashed. echo SpaceNinjaServer seems to have crashed.
)
:a :a
pause > nul pause > nul
goto a goto a

View File

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

962
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,8 @@
"scripts": { "scripts": {
"start": "node --import ./build/src/pathman.js build/src/index.js", "start": "node --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build": "tsc && copyfiles static/webui/** build", "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"verify": "tsgo --noEmit",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .", "prettier": "prettier --write .",
@ -14,27 +15,29 @@
}, },
"license": "GNU", "license": "GNU",
"dependencies": { "dependencies": {
"copyfiles": "^2.4.1", "@types/express": "^5",
"@types/morgan": "^1.9.9",
"crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"mongoose": "^8.9.4", "json-with-bigint": "^3.2.2",
"warframe-public-export-plus": "^0.5.36", "mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.52",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/express": "^5", "@rxliuli/tsgo": "^2025.3.31",
"@types/morgan": "^1.9.9", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/eslint-plugin": "^7.18", "@typescript-eslint/parser": "^8.28.0",
"@typescript-eslint/parser": "^7.18", "eslint": "^8",
"eslint": "^8.56.0", "eslint-plugin-prettier": "^5.2.5",
"eslint-plugin-prettier": "^5.2.3", "prettier": "^3.5.3",
"morgan": "^1.10.0",
"prettier": "^3.4.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0"
"typescript": ">=4.7.4 <5.6.0"
}, },
"engines": { "engines": {
"node": ">=18.15.0", "node": ">=18.15.0",

View File

@ -1,4 +1,4 @@
// Based on http://209.141.38.3/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
const fs = require("fs"); const fs = require("fs");

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,22 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getDojoClient, getGuildForRequest } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IDojoComponentDatabase } from "@/src/types/guildTypes"; import { GuildPermission, IDojoComponentDatabase } from "@/src/types/guildTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
export const changeDojoRootController: RequestHandler = async (req, res) => { export const changeDojoRootController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req); const accountId = await getAccountIdForRequest(req);
// At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the root. const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const idToNode: Record<string, INode> = {}; const idToNode: Record<string, INode> = {};
guild.DojoComponents!.forEach(x => { guild.DojoComponents.forEach(x => {
idToNode[x._id.toString()] = { idToNode[x._id.toString()] = {
component: x, component: x,
parent: undefined, parent: undefined,
@ -18,7 +25,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
}); });
let oldRoot: INode | undefined; let oldRoot: INode | undefined;
guild.DojoComponents!.forEach(x => { guild.DojoComponents.forEach(x => {
const node = idToNode[x._id.toString()]; const node = idToNode[x._id.toString()];
if (x.pi) { if (x.pi) {
idToNode[x.pi.toString()].children.push(node); idToNode[x.pi.toString()].children.push(node);
@ -47,7 +54,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
); );
top.children.forEach(x => stack.push(x)); top.children.forEach(x => stack.push(x));
} }
guild.DojoComponents!.forEach(x => { guild.DojoComponents.forEach(x => {
x._id = idMap[x._id.toString()]; x._id = idMap[x._id.toString()];
if (x.pi) { if (x.pi) {
x.pi = idMap[x.pi.toString()]; x.pi = idMap[x.pi.toString()];
@ -58,7 +65,7 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
await guild.save(); await guild.save();
res.json(getDojoClient(guild, 0)); res.json(await getDojoClient(guild, 0));
}; };
interface INode { interface INode {

View File

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

View File

@ -7,9 +7,19 @@ import { getRecipe } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency, addItem, addMiscItems, addRecipes } from "@/src/services/inventoryService"; import {
getInventory,
updateCurrency,
addItem,
addRecipes,
occupySlot,
combineInventoryChanges
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
export interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
} }
@ -37,15 +47,36 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
if (req.query.cancel) { if (req.query.cancel) {
const currencyChanges = updateCurrency(inventory, recipe.buildPrice * -1, false); const inventoryChanges: IInventoryChanges = {
addMiscItems(inventory, recipe.ingredients); ...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
await inventory.save(); await inventory.save();
// Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
res.json({
...currencyChanges,
MiscItems: recipe.ingredients
});
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
@ -68,31 +99,40 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
} }
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
} }
let InventoryChanges = {}; let InventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
const recipeChanges = [ addRecipes(inventory, [
{ {
ItemType: pendingRecipe.ItemType, ItemType: pendingRecipe.ItemType,
ItemCount: -1 ItemCount: -1
} }
]; ]);
InventoryChanges = { ...InventoryChanges, Recipes: recipeChanges };
addRecipes(inventory, recipeChanges);
} }
if (req.query.rush) { if (req.query.rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...updateCurrency(inventory, recipe.skipBuildTimePrice, true) ...updateCurrency(inventory, cost, true)
};
}
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
}; };
} }
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num)).InventoryChanges
};
await inventory.save(); await inventory.save();
res.json({ InventoryChanges }); res.json({ InventoryChanges });
} }

View File

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

View File

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

View File

@ -4,8 +4,7 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper";
import { getRandomElement, getRandomInt } from "@/src/services/rngService";
import { ExportUpgrades } from "warframe-public-export-plus"; import { ExportUpgrades } from "warframe-public-export-plus";
export const completeRandomModChallengeController: RequestHandler = async (req, res) => { export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
@ -31,17 +30,7 @@ export const completeRandomModChallengeController: RequestHandler = async (req,
// Update riven fingerprint to a randomised unveiled state // Update riven fingerprint to a randomised unveiled state
const upgrade = inventory.Upgrades.id(request.ItemId)!; const upgrade = inventory.Upgrades.id(request.ItemId)!;
const meta = ExportUpgrades[upgrade.ItemType]; const meta = ExportUpgrades[upgrade.ItemType];
const fingerprint: IUnveiledRivenFingerprint = { upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta));
compat: getRandomElement(meta.compatibleItems!),
lim: 0,
lvl: 0,
lvlReq: getRandomInt(8, 16),
pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]),
buffs: [],
curses: []
};
randomiseRivenStats(meta, fingerprint);
upgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
await inventory.save(); await inventory.save();

View File

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

View File

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

View File

@ -0,0 +1,104 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { config } from "@/src/services/configService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const contributeGuildClassController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<IContributeGuildClassRequest>(String(req.body));
const guild = (await Guild.findById(payload.GuildId))!;
// First contributor initiates ceremony and locks the pending class.
if (!guild.CeremonyContributors) {
guild.CeremonyContributors = [];
guild.CeremonyClass = guildXpToClass(guild.XP);
guild.CeremonyEndo = 0;
for (let i = guild.Class; i != guild.CeremonyClass; ++i) {
guild.CeremonyEndo += (i + 1) * 1000;
}
guild.ClassChanges ??= [];
guild.ClassChanges.push({
dateTime: new Date(),
entryType: 13,
details: guild.CeremonyClass
});
}
guild.CeremonyContributors.push(new Types.ObjectId(accountId));
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
if (guild.CeremonyContributors.length == payload.RequiredContributors) {
guild.Class = guild.CeremonyClass!;
guild.CeremonyClass = undefined;
guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
if (!config.fastClanAscension) {
// Send message to all active guild members
const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
for (const member of members) {
// somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
await createMessage(member.accountId, [
{
sndr: guild.Name,
msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
arg: [
{
Key: "RESETDATE",
Tag:
guild.CeremonyResetDate.getUTCMonth() +
"/" +
guild.CeremonyResetDate.getUTCDate() +
"/" +
(guild.CeremonyResetDate.getUTCFullYear() % 100) +
" " +
guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
":" +
guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
}
],
sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
highPriority: true
}
]);
}
}
}
await guild.save();
// Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints");
inventory.FusionPoints += guild.CeremonyEndo!;
await inventory.save();
res.json({
NumContributors: guild.CeremonyContributors.length,
FusionPointReward: guild.CeremonyEndo,
Class: guild.Class,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined
});
};
interface IContributeGuildClassRequest {
GuildId: string;
RequiredContributors: number;
}
const guildXpToClass = (xp: number): number => {
const cummXp = [
0, 11000, 34000, 69000, 114000, 168000, 231000, 302000, 381000, 68000, 563000, 665000, 774000, 891000
];
let highest = 0;
for (let i = 0; i != cummXp.length; ++i) {
if (xp < cummXp[i]) {
break;
}
highest = i;
}
return highest;
};

View File

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

View File

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

View File

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

View File

@ -1,35 +1,52 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { Guild } from "@/src/models/guildModel"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
import { addRecipes, getInventory } from "@/src/services/inventoryService";
export const createGuildController: RequestHandler = async (req, res) => { export const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<ICreateGuildRequest>(String(req.body)); const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId, status: 1 });
// Create guild on database // Create guild on database
const guild = new Guild({ const guild = new Guild({
Name: payload.guildName Name: await createUniqueClanName(payload.guildName)
}); });
await guild.save(); await guild.save();
// Update inventory // Create guild member on database
const inventory = await Inventory.findOne({ accountOwnerId: accountId }); await GuildMember.insertOne({
if (inventory) { accountId: accountId,
// Set GuildId guildId: guild._id,
inventory.GuildId = guild._id; status: 0,
rank: 0
});
// Give clan key (TODO: This should only be a blueprint) const inventory = await getInventory(accountId, "GuildId Recipes");
inventory.LevelKeys.push({ inventory.GuildId = guild._id;
ItemType: "/Lotus/Types/Keys/DojoKey", addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1 ItemCount: 1
}); }
]);
await inventory.save();
await inventory.save(); res.json({
} ...(await getGuildClient(guild, accountId)),
InventoryChanges: {
res.json(guild); Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
});
}; };
interface ICreateGuildRequest { interface ICreateGuildRequest {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,11 +20,11 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
//TODO: consider whishlist changes //TODO: consider whishlist changes
}; };
export interface IQuestKeyRewardRequest { interface IQuestKeyRewardRequest {
reward: IQuestKeyReward; reward: IQuestKeyReward;
} }
export interface IQuestKeyReward { interface IQuestKeyReward {
RewardType: string; RewardType: string;
CouponType: string; CouponType: string;
Icon: string; Icon: string;
@ -38,7 +38,7 @@ export interface IQuestKeyReward {
Duration: number; Duration: number;
CouponSku: number; CouponSku: number;
Syndicate: string; Syndicate: string;
Milestones: any[]; //Milestones: any[];
ChooseSetIndex: number; ChooseSetIndex: number;
NewSystemReward: boolean; NewSystemReward: boolean;
_id: IOid; _id: IOid;

View File

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

View File

@ -1,44 +1,139 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getGuildForRequestEx } from "@/src/services/guildService"; import {
addGuildMemberMiscItemContribution,
getGuildForRequestEx,
getGuildVault,
hasAccessToDojo,
hasGuildPermission,
processFundedGuildTechProject,
processGuildTechProjectContributionsUpdate,
removePigmentsFromGuildMembers,
scaleRequiredCount,
setGuildTechLogState
} from "@/src/services/guildService";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import {
addItem,
addMiscItems,
addRecipes,
combineInventoryChanges,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
import { GuildMember } from "@/src/models/guildModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
export const guildTechController: RequestHandler = async (req, res) => { export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory); const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest; const data = JSON.parse(String(req.body)) as TGuildTechRequest;
const action = data.Action.split(",")[0]; if (data.Action == "Sync") {
if (action == "Sync") { let needSave = false;
res.json({ const techProjects: ITechProjectClient[] = [];
TechProjects: guild.toJSON().TechProjects if (guild.TechProjects) {
}); for (const project of guild.TechProjects) {
} else if (action == "Start") { const techProject: ITechProjectClient = {
const recipe = ExportDojoRecipes.research[data.RecipeType!]; ItemType: project.ItemType,
ReqCredits: project.ReqCredits,
ReqItems: project.ReqItems,
State: project.State
};
if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate);
if (Date.now() >= project.CompletionDate.getTime()) {
needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate);
}
}
techProjects.push(techProject);
}
}
if (needSave) {
await guild.save();
}
res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= []; guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) { if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
guild.TechProjects.push({ const techProject =
ItemType: data.RecipeType!, guild.TechProjects[
ReqCredits: scaleRequiredCount(recipe.price), guild.TechProjects.push({
ReqItems: recipe.ingredients.map(x => ({ ItemType: data.RecipeType,
ItemType: x.ItemType, ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ItemCount: scaleRequiredCount(x.ItemCount) ReqItems: recipe.ingredients.map(x => ({
})), ItemType: x.ItemType,
State: 0 ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
}); })),
State: 0
}) - 1
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
}
}
} }
await guild.save(); await guild.save();
res.end(); res.end();
} else if (action == "Contribute") { } else if (data.Action == "Contribute") {
const contributions = data as IGuildTechContributeFields; if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end();
return;
}
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const contributions = data;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!; const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.VaultCredits) {
if (contributions.VaultCredits > techProject.ReqCredits) {
contributions.VaultCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.VaultCredits;
guild.VaultRegularCredits! -= contributions.VaultCredits;
}
if (contributions.RegularCredits > techProject.ReqCredits) { if (contributions.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits; contributions.RegularCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= contributions.RegularCredits; techProject.ReqCredits -= contributions.RegularCredits;
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += contributions.RegularCredits;
if (contributions.VaultMiscItems.length) {
for (const miscItem of contributions.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
}
}
}
const miscItemChanges = []; const miscItemChanges = [];
for (const miscItem of contributions.MiscItems) { for (const miscItem of contributions.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
@ -51,30 +146,29 @@ export const guildTechController: RequestHandler = async (req, res) => {
ItemType: miscItem.ItemType, ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1 ItemCount: miscItem.ItemCount * -1
}); });
addGuildMemberMiscItemContribution(guildMember, miscItem);
} }
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = { const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false);
...updateCurrency(inventory, contributions.RegularCredits, false), inventoryChanges.MiscItems = miscItemChanges;
MiscItems: miscItemChanges
};
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { // Check if research is fully funded now.
// This research is now fully funded. await processGuildTechProjectContributionsUpdate(guild, techProject);
techProject.State = 1;
const recipe = ExportDojoRecipes.research[data.RecipeType!];
techProject.CompletionDate = new Date(new Date().getTime() + recipe.time * 1000);
}
await guild.save(); await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
}); });
} else if (action == "Buy") { } else if (data.Action.split(",")[0] == "Buy") {
const purchase = data as IGuildTechBuyFields; if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const purchase = data as IGuildTechBuyRequest;
const quantity = parseInt(data.Action.split(",")[1]); const quantity = parseInt(data.Action.split(",")[1]);
const inventory = await getInventory(accountId);
const recipeChanges = [ const recipeChanges = [
{ {
ItemType: purchase.RecipeType, ItemType: purchase.RecipeType,
@ -95,24 +189,68 @@ export const guildTechController: RequestHandler = async (req, res) => {
Recipes: recipeChanges Recipes: recipeChanges
} }
}); });
} else if (data.Action == "Fabricate") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.fabrications[data.RecipeType];
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, recipe.price, false);
inventoryChanges.MiscItems = recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: x.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
combineInventoryChanges(inventoryChanges, await addItem(inventory, recipe.resultType));
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
project.State = -2;
guild.ActiveDojoColorResearch = "";
await guild.save();
await removePigmentsFromGuildMembers(guild._id);
res.end();
} else if (data.Action == "Unpause") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const project = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
project.State = 0;
guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save();
res.end();
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown guildTech action: ${data.Action}`); throw new Error(`unknown guildTech action: ${data.Action}`);
} }
}; };
type TGuildTechRequest = { type TGuildTechRequest =
Action: string; | { Action: "Sync" | "SomethingElseThatWeMightNotKnowAbout" }
} & Partial<IGuildTechStartFields> & | IGuildTechBasicRequest
Partial<IGuildTechContributeFields>; | IGuildTechContributeRequest;
interface IGuildTechStartFields { interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause";
Mode: "Guild"; Mode: "Guild";
RecipeType: string; RecipeType: string;
} }
type IGuildTechBuyFields = IGuildTechStartFields; interface IGuildTechBuyRequest {
Action: string;
Mode: "Guild";
RecipeType: string;
}
interface IGuildTechContributeFields { interface IGuildTechContributeRequest {
Action: "Contribute";
ResearchId: ""; ResearchId: "";
RecipeType: string; RecipeType: string;
RegularCredits: number; RegularCredits: number;
@ -120,8 +258,3 @@ interface IGuildTechContributeFields {
VaultCredits: number; VaultCredits: number;
VaultMiscItems: IMiscItem[]; VaultMiscItems: IMiscItem[];
} }
const scaleRequiredCount = (count: number): number => {
// The recipes in the export are for Moon clans. For now we'll just assume we only have Ghost clans.
return Math.max(1, Math.trunc(count / 100));
};

View File

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

View File

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

View File

@ -1,24 +1,26 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems, updateCurrency, addRecipes } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { import {
IConsumedSuit, IConsumedSuit,
IHelminthFoodRecord, IHelminthFoodRecord,
IInfestedFoundryClient,
IInfestedFoundryDatabase,
IInventoryClient, IInventoryClient,
IMiscItem, IMiscItem,
ITypeCount InventorySlot
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc, ExportRecipes } from "warframe-public-export-plus"; import { ExportMisc } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper"; import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import {
addInfestedFoundryXP,
applyCheatsToInfestedFoundry,
handleSubsumeCompletion
} from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -27,7 +29,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard installation // shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
} }
@ -55,19 +57,20 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard removal // shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body)); const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
// refund shard const miscItemChanges: IMiscItem[] = [];
const shard = Object.entries(colorToShard).find( if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color // refund shard
)![1]; const shard = Object.entries(colorToShard).find(
const miscItemChanges = [ ([color]) => color == suit.ArchonCrystalUpgrades![request.Slot].Color
{ )![1];
miscItemChanges.push({
ItemType: shard, ItemType: shard,
ItemCount: 1 ItemCount: 1
} });
]; addMiscItems(inventory, miscItemChanges);
addMiscItems(inventory, miscItemChanges); }
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {}; suit.ArchonCrystalUpgrades![request.Slot] = {};
@ -126,7 +129,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
let totalPercentagePointsGained = 0; let totalPercentagePointsGained = 0;
const currentUnixSeconds = Math.trunc(new Date().getTime() / 1000); const currentUnixSeconds = Math.trunc(Date.now() / 1000);
for (const contribution of request.ResourceContributions) { for (const contribution of request.ResourceContributions) {
const snack = ExportMisc.helminthSnacks[contribution.ItemType]; const snack = ExportMisc.helminthSnacks[contribution.ItemType];
@ -258,11 +261,10 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
inventory.InfestedFoundry!.ConsumedSuits ??= []; inventory.InfestedFoundry!.ConsumedSuits ??= [];
inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit); inventory.InfestedFoundry!.ConsumedSuits.push(consumedSuit);
inventory.InfestedFoundry!.LastConsumedSuit = suit; inventory.InfestedFoundry!.LastConsumedSuit = suit;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date( inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = new Date(Date.now() + 24 * 60 * 60 * 1000);
new Date().getTime() + 24 * 60 * 60 * 1000
);
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00); const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 1600_00);
addRecipes(inventory, recipeChanges); addRecipes(inventory, recipeChanges);
freeUpSlot(inventory, InventorySlot.SUITS);
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry); applyCheatsToInfestedFoundry(infestedFoundry);
@ -307,7 +309,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body)); const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000); const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.OffensiveUpgrade = request.OffensiveUpgradeType;
suit.DefensiveUpgrade = request.DefensiveUpgradeType; suit.DefensiveUpgrade = request.DefensiveUpgradeType;
suit.UpgradesExpiry = upgradesExpiry; suit.UpgradesExpiry = upgradesExpiry;
@ -354,6 +356,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
default: default:
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`); throw new Error(`unhandled infestedFoundry mode: ${String(req.query.mode)}`);
} }
}; };
@ -381,116 +384,11 @@ interface IHelminthFeedRequest {
}[]; }[];
} }
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
const recipeChanges: ITypeCount[] = [];
infestedFoundry.XP ??= 0;
const prevXP = infestedFoundry.XP;
infestedFoundry.XP += delta;
if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 3;
}
if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
ItemCount: 1
});
}
if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
}
if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 10;
}
if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
ItemCount: 1
});
}
if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
}
if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 20;
}
if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
}
if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
infestedFoundry.Slots = 1;
}
if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
ItemCount: 1
});
}
if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
ItemCount: 1
});
}
if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
ItemCount: 1
});
}
if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
ItemCount: 1
});
}
if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
ItemCount: 1
});
}
return recipeChanges;
};
interface IHelminthSubsumeRequest { interface IHelminthSubsumeRequest {
SuitId: IOid; SuitId: IOid;
Recipe: string; Recipe: string;
} }
export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => {
const [recipeType] = Object.entries(ExportRecipes).find(
([_recipeType, recipe]) =>
recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" &&
recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType
)!;
inventory.InfestedFoundry!.LastConsumedSuit = undefined;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined;
const recipeChanges: ITypeCount[] = [
{
ItemType: recipeType,
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
return recipeChanges;
};
export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => {
if (config.infiniteHelminthMaterials) {
infestedFoundry.Resources = [
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 }
];
}
};
interface IHelminthOfferingsUpdate { interface IHelminthOfferingsUpdate {
OfferingsIndex: number; OfferingsIndex: number;
SuitTypes: string[]; SuitTypes: string[];

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import allDialogue from "@/static/fixed_responses/allDialogue.json"; import allDialogue from "@/static/fixed_responses/allDialogue.json";
@ -13,13 +13,15 @@ import {
ExportResources, ExportResources,
ExportVirtuals ExportVirtuals
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { allDailyAffiliationKeys } from "@/src/services/inventoryService"; import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const accountId = await getAccountIdForRequest(request);
const inventory = await Inventory.findOne({ accountOwnerId: account._id.toString() }); const inventory = await Inventory.findOne({ accountOwnerId: accountId });
if (!inventory) { if (!inventory) {
response.status(400).json({ error: "inventory was undefined" }); response.status(400).json({ error: "inventory was undefined" });
@ -27,15 +29,57 @@ export const inventoryController: RequestHandler = async (request, response) =>
} }
// Handle daily reset // Handle daily reset
const today: number = Math.trunc(new Date().getTime() / 86400000); if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
if (account.LastLoginDay != today) {
account.LastLoginDay = today;
await account.save();
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500; inventory[key] = 16000 + inventory.PlayerLevel * 500;
} }
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.TradesRemaining = inventory.PlayerLevel;
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
if (inventory.NextRefill) {
if (config.noArgonCrystalDecay) {
inventory.FoundToday = undefined;
} else {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const today = Math.trunc(Date.now() / 86400000);
const daysPassed = today - lastLoginDay;
for (let i = 0; i != daysPassed; ++i) {
const numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0;
if (numArgonCrystals == 0) {
break;
}
const numStableArgonCrystals = Math.min(
numArgonCrystals,
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0
);
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
numArgonCrystals,
numStableArgonCrystals,
numDecayingArgonCrystals,
numDecayingArgonCrystalsToRemove
});
// Remove half of owned decaying argon crystals
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal",
ItemCount: numDecayingArgonCrystalsToRemove * -1
}
]);
// All stable argon crystals are now decaying
inventory.FoundToday = undefined;
}
}
}
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
await inventory.save(); await inventory.save();
} }
@ -67,7 +111,7 @@ export const getInventoryResponse = async (
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
} }
if (config.infinitePlatinum) { if (config.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 999999999; inventoryResponse.PremiumCreditsFree = 0;
inventoryResponse.PremiumCredits = 999999999; inventoryResponse.PremiumCredits = 999999999;
} }
if (config.infiniteEndo) { if (config.infiniteEndo) {
@ -207,8 +251,9 @@ export const getInventoryResponse = async (
} }
if (config.noDailyStandingLimits) { if (config.noDailyStandingLimits) {
const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500);
for (const key of allDailyAffiliationKeys) { for (const key of allDailyAffiliationKeys) {
inventoryResponse[key] = 999_999; inventoryResponse[key] = spoofedDailyAffiliation;
} }
} }
@ -216,19 +261,16 @@ export const getInventoryResponse = async (
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
} }
// Fix for #380
inventoryResponse.NextRefill = { $date: { $numberLong: "9999999999999" } };
// This determines if the "void fissures" tab is shown in navigation.
inventoryResponse.HasOwnedVoidProjectionsPreviously = true;
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage. // Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId()); //inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
return inventoryResponse; return inventoryResponse;
}; };
export const addString = (arr: string[], str: string): void => { const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) { if (!arr.find(x => x == str)) {
arr.push(str); arr.push(str);
} }
@ -255,15 +297,6 @@ const resourceGetParent = (resourceName: string): string | undefined => {
if (resourceName in ExportResources) { if (resourceName in ExportResources) {
return ExportResources[resourceName].parentName; return ExportResources[resourceName].parentName;
} }
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName; return ExportVirtuals[resourceName]?.parentName;
}; };
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
const catBreadHash = (name: string): number => {
let hash = 2166136261;
for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
hash = (hash * 16777619) & 0x7fffffff;
}
return hash;
};

View File

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

View File

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

View File

@ -19,10 +19,12 @@ export const loginController: RequestHandler = async (request, response) => {
? request.query.buildLabel.split(" ").join("+") ? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel; : buildConfig.buildLabel;
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") { if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
try { try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@")); const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail; let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
if (await isNameTaken(name)) { if (await isNameTaken(name)) {
let suffix = 0; let suffix = 0;
do { do {
@ -40,11 +42,10 @@ export const loginController: RequestHandler = async (request, response) => {
ForceLogoutVersion: 0, ForceLogoutVersion: 0,
ConsentNeeded: false, ConsentNeeded: false,
TrackedSettings: [], TrackedSettings: [],
Nonce: nonce, Nonce: nonce
LatestEventMessageDate: new Date(0)
}); });
logger.debug("created new account"); logger.debug("created new account");
response.json(createLoginResponse(newAccount, buildLabel)); response.json(createLoginResponse(myAddress, newAccount, buildLabel));
return; return;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -53,24 +54,37 @@ export const loginController: RequestHandler = async (request, response) => {
} }
} }
//email not found or incorrect password if (!account) {
if (!account || !isCorrectPassword(loginRequest.password, account.password)) { response.status(400).json({ error: "unknown user" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" }); response.status(400).json({ error: "incorrect login data" });
return; return;
} }
if (account.Nonce == 0 || loginRequest.ClientType != "webui") { if (loginRequest.ClientType == "webui") {
if (!account.Nonce) {
account.ClientType = "webui";
account.Nonce = nonce;
}
} else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
response.status(400).json({ error: "nonce still set" });
return;
}
account.ClientType = loginRequest.ClientType;
account.Nonce = nonce; account.Nonce = nonce;
}
if (loginRequest.ClientType != "webui") {
account.CountryCode = loginRequest.lang.toUpperCase(); account.CountryCode = loginRequest.lang.toUpperCase();
} }
await account.save(); await account.save();
response.json(createLoginResponse(account.toJSON(), buildLabel)); response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
}; };
const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => { const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
return { return {
id: account.id, id: account.id,
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
@ -84,9 +98,9 @@ const createLoginResponse = (account: IDatabaseAccountJson, buildLabel: string):
TrackedSettings: account.TrackedSettings, TrackedSettings: account.TrackedSettings,
Nonce: account.Nonce, Nonce: account.Nonce,
Groups: [], Groups: [],
IRC: config.myIrcAddresses ?? [config.myAddress], IRC: config.myIrcAddresses ?? [myAddress],
platformCDNs: config.platformCDNs, platformCDNs: [`https://${myAddress}/`],
HUB: config.hubAddress, HUB: `https://${myAddress}/api/`,
NRS: config.NRS, NRS: config.NRS,
DTLS: 99, DTLS: 99,
BuildLabel: buildLabel, BuildLabel: buildLabel,

View File

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

View File

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

View File

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

View File

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

View File

@ -47,14 +47,13 @@ import { logger } from "@/src/utils/logger";
- [ ] FpsSamples - [ ] FpsSamples
*/ */
//move credit calc in here, return MissionRewards: [] if no reward info //move credit calc in here, return MissionRewards: [] if no reward info
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => { export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString()); const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString());
logger.debug("mission report:", missionReport); logger.debug("mission report:", missionReport);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryUpdates = addMissionInventoryUpdates(inventory, missionReport); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if (missionReport.MissionStatus !== "GS_SUCCESS") { if (missionReport.MissionStatus !== "GS_SUCCESS") {
await inventory.save(); await inventory.save();

View File

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

View File

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

View File

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

View File

@ -0,0 +1,210 @@
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const nemesisController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
if ((req.query.mode as string) == "f") {
const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
const inventory = await getInventory(accountId, body.Category + " WeaponBin");
const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!;
const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!;
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
// Update destination damage type if desired
if (body.UseSourceDmgType) {
destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
}
// Upgrade destination damage value
const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
let newDamage = Math.max(destDamage, sourceDamage) * 1.1;
if (newDamage >= 0.5794998) {
newDamage = 0.6;
}
destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
// Commit fingerprint
destWeapon.UpgradeFingerprint = JSON.stringify(destFingerprint);
// Remove source weapon
inventory[body.Category].pull({ _id: body.SourceWeapon.$oid });
freeUpSlot(inventory, InventorySlot.WEAPONS);
await inventory.save();
res.json({
InventoryChanges: {
[body.Category]: [destWeapon.toJSON()],
RemovedIdItems: [{ ItemId: body.SourceWeapon }]
}
});
} else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction);
let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[0]) {
guessResult = 1 + i;
break;
}
}
} else {
for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i]) {
++guessResult;
}
}
}
res.json({ GuessResult: guessResult });
} else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp);
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
let weapons: readonly string[];
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
weapons = kuvaLichVersionSixWeapons;
} else if (
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
) {
weapons = corpusVersionThreeWeapons;
} else {
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
}
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx;
do {
const weapon = weapons[weaponIdx];
if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
break;
}
weaponIdx = (weaponIdx + 1) % weapons.length;
} while (weaponIdx != initialWeaponIdx);
}
inventory.Nemesis = {
fp: body.target.fp,
manifest: body.target.manifest,
KillingSuit: body.target.KillingSuit,
killingDamageType: body.target.killingDamageType,
ShoulderHelmet: body.target.ShoulderHelmet,
WeaponIdx: weaponIdx,
AgentIdx: body.target.AgentIdx,
BirthNode: body.target.BirthNode,
Faction: body.target.Faction,
Rank: 0,
k: false,
Traded: false,
d: new Date(),
InfNodes: getInfNodes(body.target.Faction, 0),
GuessHistory: [],
Hints: [],
HintProgress: 0,
Weakened: body.target.Weakened,
PrevOwners: 0,
HenchmenKilled: 0,
SecondInCommand: body.target.SecondInCommand,
MissionCount: 0,
LastEnc: 0
};
await inventory.save();
res.json({
target: inventory.toJSON().Nemesis
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
}
};
interface IValenceFusionRequest {
DestWeapon: IOid;
SourceWeapon: IOid;
Category: TEquipmentKey;
UseSourceDmgType: boolean;
}
interface INemesisStartRequest {
target: {
fp: number | bigint;
manifest: string;
KillingSuit: string;
killingDamageType: number;
ShoulderHelmet: string;
DisallowedWeapons: string[];
WeaponIdx: number;
AgentIdx: number;
BirthNode: string;
Faction: string;
Rank: number;
k: boolean;
Traded: boolean;
d: IMongoDate;
InfNodes: [];
GuessHistory: [];
Hints: [];
HintProgress: number;
Weakened: boolean;
PrevOwners: number;
HenchmenKilled: number;
MissionCount?: number; // Added in 38.5.0
LastEnc?: number; // Added in 38.5.0
SecondInCommand: boolean;
};
}
interface INemesisPrespawnCheckRequest {
guess: number[]; // .length == 3
potency?: number[];
}
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,34 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { RequestHandler } from "express";
import glyphCodes from "@/static/fixed_responses/glyphsCodes.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addItem, getInventory } from "@/src/services/inventoryService";
export const redeemPromoCodeController: RequestHandler = async (req, res) => {
const body = getJSONfromString<IRedeemPromoCodeRequest>(String(req.body));
if (!(body.codeId in glyphCodes)) {
res.status(400).send("INVALID_CODE").end();
return;
}
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "FlavourItems");
const acquiredGlyphs: string[] = [];
for (const glyph of (glyphCodes as Record<string, string[]>)[body.codeId]) {
if (!inventory.FlavourItems.find(x => x.ItemType == glyph)) {
acquiredGlyphs.push(glyph);
await addItem(inventory, glyph);
}
}
if (acquiredGlyphs.length == 0) {
res.status(400).send("USED_CODE").end();
return;
}
await inventory.save();
res.json({
FlavourItems: acquiredGlyphs
});
};
interface IRedeemPromoCodeRequest {
codeId: string;
}

View File

@ -0,0 +1,23 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const releasePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "RegularCredits KubrowPets");
const payload = getJSONfromString<IReleasePetRequest>(String(req.body));
const inventoryChanges = updateCurrency(inventory, 25000, false);
inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }];
inventory.KubrowPets.pull({ _id: payload.petId });
await inventory.save();
res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
};
interface IReleasePetRequest {
recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe";
petId: string;
}

View File

@ -0,0 +1,38 @@
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { deleteAlliance } from "@/src/services/guildService";
import { getAccountForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const removeFromAllianceController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
let allianceMember = (await AllianceMember.findOne({ guildId: guildMember.guildId }))!;
if (!guildMember.guildId.equals(req.query.guildId as string)) {
// Removing a guild that is not our own needs additional permissions
if (!(allianceMember.Permissions & GuildPermission.Ruler)) {
res.status(400).json({ Error: 104 });
return;
}
// Update allianceMember to point to the alliance to kick
allianceMember = (await AllianceMember.findOne({ guildId: req.query.guildId }))!;
}
if (allianceMember.Permissions & GuildPermission.Ruler) {
await deleteAlliance(allianceMember.allianceId);
} else {
await Promise.all([
await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }),
await AllianceMember.deleteOne({ _id: allianceMember._id })
]);
}
res.end();
};

View File

@ -0,0 +1,88 @@
import { GuildMember } from "@/src/models/guildModel";
import { Inbox } from "@/src/models/inboxModel";
import { Account } from "@/src/models/loginModel";
import { deleteGuild, getGuildForRequest, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const removeFromGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const guild = await getGuildForRequest(req);
const payload = JSON.parse(String(req.body)) as IRemoveFromGuildRequest;
const isKick = !account._id.equals(payload.userId);
if (isKick && !(await hasGuildPermission(guild, account._id, GuildPermission.Regulator))) {
res.status(400).json("Invalid permission");
return;
}
const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!;
if (guildMember.rank == 0) {
await deleteGuild(guild._id);
} else {
if (guildMember.status == 0) {
const inventory = await getInventory(payload.userId, "GuildId LevelKeys Recipes");
inventory.GuildId = undefined;
removeDojoKeyItems(inventory);
await inventory.save();
} else if (guildMember.status == 1) {
// TOVERIFY: Is this inbox message actually sent on live?
await createMessage(guildMember.accountId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Clan/RejectedFromClan",
sub: "/Lotus/Language/Clan/RejectedFromClanHeader",
arg: [
{
Key: "PLAYER_NAME",
Tag: (await Account.findOne({ _id: guildMember.accountId }, "DisplayName"))!.DisplayName
},
{
Key: "CLAN_NAME",
Tag: guild.Name
}
]
// TOVERIFY: If this message is sent on live, is it highPriority?
}
]);
} else if (guildMember.status == 2) {
// Delete the inbox message for the invite
await Inbox.deleteOne({
ownerId: guildMember.accountId,
contextInfo: guild._id.toString(),
acceptAction: "GUILD_INVITE"
});
}
await GuildMember.deleteOne({ _id: guildMember._id });
guild.RosterActivity ??= [];
if (isKick) {
const kickee = (await Account.findById(payload.userId))!;
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 12,
details: getSuffixedName(kickee) + "," + getSuffixedName(account)
});
} else {
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 7,
details: getSuffixedName(account)
});
}
await guild.save();
}
res.json({
_id: payload.userId,
ItemToRemove: "/Lotus/Types/Keys/DojoKey",
RecipeToRemove: "/Lotus/Types/Keys/DojoKeyBlueprint"
});
};
interface IRemoveFromGuildRequest {
userId: string;
kicker?: string;
}

View File

@ -2,43 +2,54 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; 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 { IUnveiledRivenFingerprint, randomiseRivenStats } from "@/src/helpers/rivenFingerprintHelper"; import { createUnveiledRivenFingerprint, randomiseRivenStats, RivenFingerprint } from "@/src/helpers/rivenHelper";
import { ExportUpgrades } from "warframe-public-export-plus"; import { ExportUpgrades } from "warframe-public-export-plus";
import { IOid } from "@/src/types/commonTypes";
export const rerollRandomModController: RequestHandler = async (req, res) => { export const rerollRandomModController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const request = getJSONfromString<RerollRandomModRequest>(String(req.body)); const request = getJSONfromString<RerollRandomModRequest>(String(req.body));
if ("ItemIds" in request) { if ("ItemIds" in request) {
const inventory = await getInventory(accountId, "Upgrades MiscItems"); const inventory = await getInventory(accountId, "Upgrades MiscItems");
const upgrade = inventory.Upgrades.id(request.ItemIds[0])!; const changes: IChange[] = [];
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as IUnveiledRivenFingerprint; let totalKuvaCost = 0;
request.ItemIds.forEach(itemId => {
const upgrade = inventory.Upgrades.id(itemId)!;
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint!) as RivenFingerprint;
if ("challenge" in fingerprint) {
upgrade.UpgradeFingerprint = JSON.stringify(
createUnveiledRivenFingerprint(ExportUpgrades[upgrade.ItemType])
);
} else {
fingerprint.rerolls ??= 0;
const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500;
totalKuvaCost += kuvaCost;
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/Kuva",
ItemCount: kuvaCost * -1
}
]);
fingerprint.rerolls ??= 0; fingerprint.rerolls++;
const kuvaCost = fingerprint.rerolls < rerollCosts.length ? rerollCosts[fingerprint.rerolls] : 3500; upgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
addMiscItems(inventory, [
{ randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint);
ItemType: "/Lotus/Types/Items/MiscItems/Kuva", upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint);
ItemCount: kuvaCost * -1
} }
]);
fingerprint.rerolls++; changes.push({
upgrade.UpgradeFingerprint = JSON.stringify(fingerprint); ItemId: { $oid: request.ItemIds[0] },
UpgradeFingerprint: upgrade.UpgradeFingerprint,
randomiseRivenStats(ExportUpgrades[upgrade.ItemType], fingerprint); PendingRerollFingerprint: upgrade.PendingRerollFingerprint
upgrade.PendingRerollFingerprint = JSON.stringify(fingerprint); });
});
await inventory.save(); await inventory.save();
res.json({ res.json({
changes: [ changes: changes,
{ cost: totalKuvaCost
ItemId: { $oid: request.ItemIds[0] },
UpgradeFingerprint: upgrade.UpgradeFingerprint,
PendingRerollFingerprint: upgrade.PendingRerollFingerprint
}
],
cost: kuvaCost
}); });
} else { } else {
const inventory = await getInventory(accountId, "Upgrades"); const inventory = await getInventory(accountId, "Upgrades");
@ -63,4 +74,10 @@ interface AwDangitRequest {
CommitReroll: boolean; CommitReroll: boolean;
} }
interface IChange {
ItemId: IOid;
UpgradeFingerprint?: string;
PendingRerollFingerprint?: string;
}
const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150]; const rerollCosts = [900, 1000, 1200, 1400, 1700, 2000, 2350, 2750, 3150];

View File

@ -0,0 +1,33 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const retrievePetFromStasisController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets");
const data = getJSONfromString<IRetrievePetFromStasisRequest>(String(req.body));
let oldPetId: string | undefined;
for (const pet of inventory.KubrowPets) {
if (pet.Details!.Status == Status.StatusAvailable) {
pet.Details!.Status = Status.StatusStasis;
oldPetId = pet._id.toString();
break;
}
}
inventory.KubrowPets.id(data.petId)!.Details!.Status = Status.StatusAvailable;
await inventory.save();
res.json({
petId: data.petId,
oldPetId,
status: Status.StatusAvailable
});
};
interface IRetrievePetFromStasisRequest {
petId: string;
}

View File

@ -81,5 +81,11 @@ interface SaveCompletedDialogueRequest {
Booleans: string[]; Booleans: string[];
ResetBooleans: string[]; ResetBooleans: string[];
Data: ICompletedDialogue; Data: ICompletedDialogue;
OtherDialogueInfos: string[]; // unsure OtherDialogueInfos: IOtherDialogueInfo[]; // unsure
}
interface IOtherDialogueInfo {
Dialogue: string;
Tag: string;
Value: number;
} }

View File

@ -13,10 +13,10 @@ const saveSettingsController: RequestHandler = async (req, res): Promise<void> =
const settingResults = getJSONfromString<ISaveSettingsRequest>(String(req.body)); const settingResults = getJSONfromString<ISaveSettingsRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId, "Settings");
inventory.Settings = Object.assign(inventory.Settings, settingResults.Settings); inventory.Settings = Object.assign(inventory.Settings ?? {}, settingResults.Settings);
await inventory.save(); await inventory.save();
res.json(inventory.Settings); res.json({ Settings: inventory.Settings });
}; };
export { saveSettingsController }; export { saveSettingsController };

View File

@ -0,0 +1,25 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const saveVaultAutoContributeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = (await Guild.findById(inventory.GuildId!, "Ranks AutoContributeFromVault"))!;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) {
res.status(400).send("Invalid permission").end();
return;
}
const data = getJSONfromString<ISetVaultAutoContributeRequest>(String(req.body));
guild.AutoContributeFromVault = data.autoContributeFromVault;
await guild.save();
res.end();
};
interface ISetVaultAutoContributeRequest {
autoContributeFromVault: boolean;
}

View File

@ -1,11 +1,54 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, addMods, addRecipes, addMiscItems, addConsumables } from "@/src/services/inventoryService"; import {
getInventory,
addMods,
addRecipes,
addMiscItems,
addConsumables,
freeUpSlot
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
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;
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const requiredFields = new Set();
if (payload.SellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
requiredFields.add("FusionPoints");
} else {
requiredFields.add("MiscItems");
}
for (const key of Object.keys(payload.Items)) {
requiredFields.add(key);
}
if (requiredFields.has("Upgrades")) {
requiredFields.add("RawUpgrades");
}
if (payload.Items.Suits) {
requiredFields.add(InventorySlot.SUITS);
}
if (payload.Items.LongGuns || payload.Items.Pistols || payload.Items.Melee) {
requiredFields.add(InventorySlot.WEAPONS);
}
if (payload.Items.SpaceSuits) {
requiredFields.add(InventorySlot.SPACESUITS);
}
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
requiredFields.add(InventorySlot.SENTINELS);
}
if (payload.Items.OperatorAmps) {
requiredFields.add(InventorySlot.AMPS);
}
if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS);
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
// Give currency // Give currency
if (payload.SellCurrency == "SC_RegularCredits") { if (payload.SellCurrency == "SC_RegularCredits") {
@ -34,56 +77,72 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.Suits) { if (payload.Items.Suits) {
payload.Items.Suits.forEach(sellItem => { payload.Items.Suits.forEach(sellItem => {
inventory.Suits.pull({ _id: sellItem.String }); inventory.Suits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SUITS);
}); });
} }
if (payload.Items.LongGuns) { if (payload.Items.LongGuns) {
payload.Items.LongGuns.forEach(sellItem => { payload.Items.LongGuns.forEach(sellItem => {
inventory.LongGuns.pull({ _id: sellItem.String }); inventory.LongGuns.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.WEAPONS);
}); });
} }
if (payload.Items.Pistols) { if (payload.Items.Pistols) {
payload.Items.Pistols.forEach(sellItem => { payload.Items.Pistols.forEach(sellItem => {
inventory.Pistols.pull({ _id: sellItem.String }); inventory.Pistols.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.WEAPONS);
}); });
} }
if (payload.Items.Melee) { if (payload.Items.Melee) {
payload.Items.Melee.forEach(sellItem => { payload.Items.Melee.forEach(sellItem => {
inventory.Melee.pull({ _id: sellItem.String }); inventory.Melee.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.WEAPONS);
}); });
} }
if (payload.Items.SpaceSuits) { if (payload.Items.SpaceSuits) {
payload.Items.SpaceSuits.forEach(sellItem => { payload.Items.SpaceSuits.forEach(sellItem => {
inventory.SpaceSuits.pull({ _id: sellItem.String }); inventory.SpaceSuits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACESUITS);
}); });
} }
if (payload.Items.SpaceGuns) { if (payload.Items.SpaceGuns) {
payload.Items.SpaceGuns.forEach(sellItem => { payload.Items.SpaceGuns.forEach(sellItem => {
inventory.SpaceGuns.pull({ _id: sellItem.String }); inventory.SpaceGuns.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
}); });
} }
if (payload.Items.SpaceMelee) { if (payload.Items.SpaceMelee) {
payload.Items.SpaceMelee.forEach(sellItem => { payload.Items.SpaceMelee.forEach(sellItem => {
inventory.SpaceMelee.pull({ _id: sellItem.String }); inventory.SpaceMelee.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
}); });
} }
if (payload.Items.Sentinels) { if (payload.Items.Sentinels) {
payload.Items.Sentinels.forEach(sellItem => { payload.Items.Sentinels.forEach(sellItem => {
inventory.Sentinels.pull({ _id: sellItem.String }); inventory.Sentinels.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
}); });
} }
if (payload.Items.SentinelWeapons) { if (payload.Items.SentinelWeapons) {
payload.Items.SentinelWeapons.forEach(sellItem => { payload.Items.SentinelWeapons.forEach(sellItem => {
inventory.SentinelWeapons.pull({ _id: sellItem.String }); inventory.SentinelWeapons.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
}); });
} }
if (payload.Items.OperatorAmps) { if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => { payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String }); inventory.OperatorAmps.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.AMPS);
}); });
} }
if (payload.Items.Hoverboards) { if (payload.Items.Hoverboards) {
payload.Items.Hoverboards.forEach(sellItem => { payload.Items.Hoverboards.forEach(sellItem => {
inventory.Hoverboards.pull({ _id: sellItem.String }); inventory.Hoverboards.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SPACESUITS);
});
}
if (payload.Items.Drones) {
payload.Items.Drones.forEach(sellItem => {
inventory.Drones.pull({ _id: sellItem.String });
}); });
} }
if (payload.Items.Consumables) { if (payload.Items.Consumables) {
@ -152,6 +211,7 @@ interface ISellRequest {
SentinelWeapons?: ISellItem[]; SentinelWeapons?: ISellItem[];
OperatorAmps?: ISellItem[]; OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[]; Hoverboards?: ISellItem[];
Drones?: ISellItem[];
}; };
SellPrice: number; SellPrice: number;
SellCurrency: SellCurrency:

View File

@ -0,0 +1,38 @@
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setAllianceGuildPermissionsController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).end();
return;
}
// Check guild is the creator of the alliance and don't allow changing of own permissions. (Technically changing permissions requires the Promoter permission, but both are exclusive to the creator guild.)
const allianceMember = (await AllianceMember.findOne({
guildId: guildMember.guildId,
Pending: false
}))!;
if (
!(allianceMember.Permissions & GuildPermission.Ruler) ||
allianceMember.guildId.equals(req.query.guildId as string)
) {
res.status(400).end();
return;
}
const targetAllianceMember = (await AllianceMember.findOne({
allianceId: allianceMember.allianceId,
guildId: req.query.guildId
}))!;
targetAllianceMember.Permissions =
parseInt(req.query.perms as string) &
(GuildPermission.Recruiter | GuildPermission.Treasurer | GuildPermission.ChatModerator);
await targetAllianceMember.save();
res.end();
};

View File

@ -4,7 +4,7 @@ import { getDojoClient, getGuildForRequest } from "@/src/services/guildService";
export const setDojoComponentMessageController: RequestHandler = async (req, res) => { export const setDojoComponentMessageController: RequestHandler = async (req, res) => {
const guild = await getGuildForRequest(req); const guild = await getGuildForRequest(req);
// At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the message. // At this point, we know that a member of the guild is making this request. Assuming they are allowed to change the message.
const component = guild.DojoComponents!.find(x => x._id.equals(req.query.componentId as string))!; const component = guild.DojoComponents.id(req.query.componentId as string)!;
const payload = JSON.parse(String(req.body)) as SetDojoComponentMessageRequest; const payload = JSON.parse(String(req.body)) as SetDojoComponentMessageRequest;
if ("Name" in payload) { if ("Name" in payload) {
component.Name = payload.Name; component.Name = payload.Name;
@ -12,7 +12,7 @@ export const setDojoComponentMessageController: RequestHandler = async (req, res
component.Message = payload.Message; component.Message = payload.Message;
} }
await guild.save(); await guild.save();
res.json(getDojoClient(guild, 1)); res.json(await getDojoClient(guild, 0, component._id));
}; };
type SetDojoComponentMessageRequest = { Name: string } | { Message: string }; type SetDojoComponentMessageRequest = { Name: string } | { Message: string };

View File

@ -1,14 +1,21 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const setEquippedInstrumentController: RequestHandler = async (req, res) => { export const setEquippedInstrumentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const body = getJSONfromString<ISetEquippedInstrumentRequest>(String(req.body)); const body = getJSONfromString<ISetEquippedInstrumentRequest>(String(req.body));
inventory.EquippedInstrument = body.Instrument;
await inventory.save(); await Inventory.updateOne(
{
accountOwnerId: accountId
},
{
EquippedInstrument: body.Instrument
}
);
res.end(); res.end();
}; };

View File

@ -0,0 +1,59 @@
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermissionEx } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setGuildMotdController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId");
const guild = (await Guild.findById(inventory.GuildId!))!;
const member = (await GuildMember.findOne({ accountId: account._id, guildId: guild._id }))!;
const IsLongMOTD = "longMOTD" in req.query;
const MOTD = req.body ? String(req.body) : undefined;
if ("alliance" in req.query) {
if (member.rank > 1) {
res.status(400).json("Invalid permission");
return;
}
const alliance = (await Alliance.findById(guild.AllianceId!))!;
const motd = MOTD
? ({
message: MOTD,
authorName: getSuffixedName(account),
authorGuildName: guild.Name
} satisfies ILongMOTD)
: undefined;
if (IsLongMOTD) {
alliance.LongMOTD = motd;
} else {
alliance.MOTD = motd;
}
await alliance.save();
} else {
if (!hasGuildPermissionEx(guild, member, GuildPermission.Herald)) {
res.status(400).json("Invalid permission");
return;
}
if (IsLongMOTD) {
if (MOTD) {
guild.LongMOTD = {
message: MOTD,
authorName: getSuffixedName(account)
};
} else {
guild.LongMOTD = undefined;
}
} else {
guild.MOTD = MOTD ?? "";
}
await guild.save();
}
res.json({ IsLongMOTD, MOTD });
};

View File

@ -1,11 +1,18 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
export const setSupportedSyndicateController: RequestHandler = async (req, res) => { export const setSupportedSyndicateController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
inventory.SupportedSyndicate = req.query.syndicate as string; await Inventory.updateOne(
await inventory.save(); {
accountOwnerId: accountId
},
{
SupportedSyndicate: req.query.syndicate as string
}
);
res.end(); res.end();
}; };

View File

@ -1,20 +1,25 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { WeaponTypeInternal } from "@/src/services/itemDataService"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { export const setWeaponSkillTreeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body)); const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body));
const item = inventory[req.query.Category as WeaponTypeInternal].find( if (equipmentKeys.indexOf(req.query.Category as TEquipmentKey) != -1) {
item => item._id.toString() == (req.query.ItemId as string) await Inventory.updateOne(
)!; {
item.SkillTree = payload.SkillTree; accountOwnerId: accountId,
[`${req.query.Category as string}._id`]: req.query.ItemId as string
},
{
[`${req.query.Category as string}.$.SkillTree`]: payload.SkillTree
}
);
}
await inventory.save();
res.end(); res.end();
}; };

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