Compare commits

..

1 Commits

Author SHA1 Message Date
0534ae001a fix: use IMongoDate for EntratiVaultCountResetDate in inventory response
All checks were successful
Build / build (20) (push) Successful in 41s
Build / build (22) (push) Successful in 1m10s
Build / build (20) (pull_request) Successful in 1m11s
Build / build (22) (pull_request) Successful in 1m16s
Build / build (18) (push) Successful in 1m13s
Build / build (18) (pull_request) Successful in 43s
2025-03-23 17:57:46 +01:00
219 changed files with 5188 additions and 14449 deletions

View File

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

2
.gitattributes vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,8 +10,5 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
- `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 ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.

View File

@ -1,6 +1,7 @@
@echo off
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
git stash
git reset --hard origin/main

View File

@ -19,7 +19,6 @@
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
@ -30,27 +29,19 @@
"unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false,
"noDailyStandingLimits": false,
"noDailyFocusLimit": false,
"noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true,
"noDeathMarks": false,
"noKimCooldowns": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,
"fastClanAscension": false,
"spoofMasteryRank": -1,
"worldState": {
"events": {
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true,
"lockTime": 0
"starDays": true
}
}

610
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,12 +4,10 @@
"description": "WF Emulator",
"main": "index.ts",
"scripts": {
"start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
"start": "node --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"verify": "tsgo --noEmit",
"build": "tsc && copyfiles static/webui/** build",
"lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js"
@ -18,25 +16,24 @@
"dependencies": {
"@types/express": "^5",
"@types/morgan": "^1.9.9",
"copyfiles": "^2.4.1",
"crc-32": "^1.2.2",
"express": "^5",
"json-with-bigint": "^3.2.2",
"json-with-bigint": "^3.2.1",
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.47",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
},
"devDependencies": {
"@rxliuli/tsgo": "^2025.3.31",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^8",
"eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3",
"@typescript-eslint/eslint-plugin": "^7.18",
"@typescript-eslint/parser": "^7.18",
"eslint": "^8.56.0",
"eslint-plugin-prettier": "^5.2.3",
"prettier": "^3.4.2",
"ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0"
},

View File

@ -4,7 +4,7 @@
const fs = require("fs");
function extractStrings(content) {
const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
let matches;
const strings = {};
while ((matches = regex.exec(content)) !== null) {
@ -15,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
const sourceStrings = extractStrings(source);
const sourceLines = source.substring(0, source.length - 1).split("\n");
const sourceLines = source.split("\n");
fs.readdirSync("../static/webui/translations").forEach(file => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
}
});
} else {
} else if (line.length) {
fs.writeSync(fileHandle, line + "\n");
}
});

View File

@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express();
app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
// 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"] == "e") {
if (req.headers["content-encoding"] == "ezip") {
req.headers["content-encoding"] = undefined;
}
next();
@ -26,7 +26,7 @@ app.use((req, _res, next) => {
app.use(bodyParser.raw());
app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text({ limit: "4mb" }));
app.use(bodyParser.text());
app.use(requestLogger);
app.use("/api", apiRouter);

View File

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

View File

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

View File

@ -17,7 +17,7 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
ItemCount: -1
}
]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!;
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({

View File

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

View File

@ -1,117 +0,0 @@
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { createMessage } from "@/src/services/inboxService";
import { 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

@ -3,103 +3,75 @@ 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 { getAccountForRequest, 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();
const account = await Account.findOne({ DisplayName: payload.UserName });
if (!account) {
res.status(400).json("Username does not exist");
return;
}
const guild = (await Guild.findOne({ _id: payload.GuildId.$oid }, "Name"))!;
const senderAccount = await getAccountForRequest(req);
if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission");
}
if (
await GuildMember.exists({
accountId: account._id,
guildId: payload.GuildId.$oid
})
) {
res.status(400).json("User already invited to clan");
return;
}
await GuildMember.insertOne({
accountId: account._id,
guildId: payload.GuildId.$oid,
status: 2 // outgoing invite
});
const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType");
await createMessage(account._id.toString(), [
{
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 });
};
interface IAddToGuildRequest {
UserName?: string;
UserName: string;
GuildId: IOid;
RequestMsg?: string;
}

View File

@ -28,7 +28,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
});
const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
@ -53,56 +53,33 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
RARE: 0,
LEGENDARY: 0
};
let forcedPolarity: string | undefined;
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else {
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
}
if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
forcedPolarity = "AP_ATTACK";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
forcedPolarity = "AP_DEFENSE";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/TacticTransmuteCore") {
forcedPolarity = "AP_TACTIC";
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
});
// 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) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
});
let newModType: string | undefined;
for (const specialModSet of specialModSets) {
if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
newModType = getRandomElement(specialModSet);
break;
}
}
if (!newModType) {
// Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
});
newModType = getRandomWeightedReward(options, weights)!.uniqueName;
}
const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
addMods(inventory, [
{
ItemType: newModType,
@ -145,34 +122,3 @@ interface IAgnosticUpgradeClient {
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [
[
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
],
[
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
],
[
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
]
];

View File

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

View File

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

View File

@ -11,16 +11,15 @@ import {
getInventory,
updateCurrency,
addItem,
addMiscItems,
addRecipes,
occupySlot,
combineInventoryChanges
occupySlot
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
interface IClaimCompletedRecipeRequest {
export interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
}
@ -52,14 +51,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
const nonMiscItemIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
nonMiscItemIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
@ -67,21 +66,20 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
const miscItemChanges: IMiscItem[] = [];
recipe.ingredients.forEach(ingredient => {
if (!nonMiscItemIngredients.has(ingredient.ItemType)) {
miscItemChanges.push(ingredient);
}
}
});
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
@ -101,15 +99,9 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
}
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
}
let InventoryChanges: IInventoryChanges = {};
let InventoryChanges = {};
if (recipe.consumeOnUse) {
addRecipes(inventory, [
{
@ -119,31 +111,16 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
]);
}
if (req.query.rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
InventoryChanges = {
...InventoryChanges,
...updateCurrency(inventory, cost, true)
};
}
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
...updateCurrency(inventory, recipe.skipBuildTimePrice, true)
};
}
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
};
await inventory.save();
res.json({ InventoryChanges, BrandedSuits });
res.json({ InventoryChanges });
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,58 +1,26 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import {
deleteGuild,
getGuildClient,
giveClanKey,
hasGuildPermission,
removeDojoKeyItems
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
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) => {
export const confirmGuildInvitationController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const invitedGuildMember = await GuildMember.findOne({
const guildMember = await GuildMember.findOne({
accountId: account._id,
guildId: req.query.clanId as string
});
if (invitedGuildMember && invitedGuildMember.status == 2) {
let inventoryChanges: IInventoryChanges = {};
if (guildMember) {
guildMember.status = 0;
await guildMember.save();
// 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();
await updateInventoryForConfirmedGuildJoin(
account._id.toString(),
new Types.ObjectId(req.query.clanId as string)
);
if (guildMember.rank == 0) {
await deleteGuild(guildMember.guildId);
}
}
const guild = (await Guild.findOne({ _id: req.query.clanId as string }))!;
// Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates.
invitedGuildMember.status = 0;
await invitedGuildMember.save();
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId: account._id, status: 1 });
// Update inventory of new member
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory, inventoryChanges);
await inventory.save();
const guild = (await Guild.findById(req.query.clanId as string))!;
// Add join to clan log
guild.RosterActivity ??= [];
guild.RosterActivity.push({
dateTime: new Date(),
@ -63,56 +31,16 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
res.json({
...(await getGuildClient(guild, account._id.toString())),
InventoryChanges: inventoryChanges
InventoryChanges: {
Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
});
} 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 LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
giveClanKey(inventory);
await inventory.save();
// Add join to clan log
const account = (await Account.findOne({ _id: guildMember.accountId }))!;
guild.RosterActivity ??= [];
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 6,
details: getSuffixedName(account)
});
newMembers.push(account._id.toString());
}
await guild.save();
res.json({
NewMembers: newMembers
});
};

View File

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

View File

@ -1,7 +1,6 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
addGuildMemberMiscItemContribution,
getDojoClient,
getGuildForRequestEx,
hasAccessToDojo,
@ -11,7 +10,7 @@ import {
} 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 { IDojoContributable } from "@/src/types/guildTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
@ -36,10 +35,6 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
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)!;
@ -50,7 +45,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
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);
processContribution(guild, request, inventory, inventoryChanges, meta, component);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (component.CompletionTime) {
setDojoRoomLogFunded(guild, component);
@ -60,11 +55,12 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
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);
processContribution(guild, request, inventory, inventoryChanges, meta, deco);
}
}
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
await guild.save();
await inventory.save();
res.json({
...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges
@ -73,7 +69,6 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
const processContribution = (
guild: TGuildDatabaseDocument,
guildMember: IGuildMemberDatabase,
request: IContributeToDojoComponentRequest,
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
@ -85,18 +80,15 @@ const processContribution = (
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)) {
if (component.RegularCredits > scaleRequiredCount(meta.price)) {
guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price);
component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price);
component.RegularCredits = scaleRequiredCount(meta.price);
}
component.MiscItems ??= [];
@ -107,10 +99,10 @@ const processContribution = (
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
scaleRequiredCount(ingredientMeta.ItemCount)
) {
ingredientContribution.ItemCount =
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
}
componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else {
@ -128,10 +120,10 @@ const processContribution = (
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
scaleRequiredCount(ingredientMeta.ItemCount)
) {
ingredientContribution.ItemCount =
scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
}
componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else {
@ -141,21 +133,16 @@ const processContribution = (
ItemType: ingredientContribution.ItemType,
ItemCount: ingredientContribution.ItemCount * -1
});
addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
}
if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
if (component.RegularCredits >= scaleRequiredCount(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)
) {
if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) {
fullyFunded = false;
break;
}

View File

@ -1,104 +1,43 @@
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 { getGuildForRequestEx } from "@/src/services/guildService";
import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } 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 inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
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);
guild.VaultMiscItems ??= [];
for (const item of request.MiscItems) {
if (guildMember) {
addGuildMemberMiscItemContribution(guildMember, item);
}
guild.VaultMiscItems.push(item);
addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
if (request.ShipDecorations.length) {
addVaultShipDecos(guild, request.ShipDecorations);
guild.VaultShipDecorations ??= [];
for (const item of request.ShipDecorations) {
if (guildMember) {
addGuildMemberShipDecoContribution(guildMember, item);
}
guild.VaultShipDecorations.push(item);
addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
}
}
if (request.FusionTreasures.length) {
addVaultFusionTreasures(guild, request.FusionTreasures);
guild.VaultFusionTreasures ??= [];
for (const item of request.FusionTreasures) {
guild.VaultFusionTreasures.push(item);
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);
await guild.save();
await inventory.save();
res.end();
};
@ -107,7 +46,4 @@ interface IContributeToVaultRequest {
MiscItems: IMiscItem[];
ShipDecorations: ITypeCount[];
FusionTreasures: IFusionTreasure[];
Alliance?: boolean;
FromVault?: boolean;
GuildVault?: string;
}

View File

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

View File

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

View File

@ -1,28 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
await inventory.save();
res.json({
crewMemberId: data.crewMember.ItemId.$oid,
NemesisFingerprint: data.crewMember.NemesisFingerprint
});
};
interface ICrewMembersRequest {
crewMember: ICrewMemberClient;
}

View File

@ -1,84 +0,0 @@
import {
addCrewShipSalvagedWeaponSkin,
addCrewShipRawSalvage,
getInventory,
addEquipment
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(
accountId,
"CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage"
);
const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
const inventoryChanges: IInventoryChanges = {};
if (payload.ItemType in ExportCustoms) {
const meta = ExportCustoms[payload.ItemType];
let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] };
if (meta.subroutines) {
upgradeFingerprint = {
SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1),
...upgradeFingerprint
};
}
for (const upgrade of meta.randomisedUpgrades!) {
upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
addCrewShipSalvagedWeaponSkin(
inventory,
payload.ItemType,
JSON.stringify(upgradeFingerprint),
inventoryChanges
);
} else {
const meta = ExportRailjackWeapons[payload.ItemType];
let defaultOverwrites: Partial<IEquipmentDatabase> | undefined;
if (meta.defaultUpgrades?.[0]) {
const upgradeType = meta.defaultUpgrades[0].ItemType;
const upgradeMeta = ExportUpgrades[upgradeType];
const buffs: IFingerprintStat[] = [];
for (const buff of upgradeMeta.upgradeEntries!) {
buffs.push({
Tag: buff.tag,
Value: Math.trunc(Math.random() * 0x40000000)
});
}
defaultOverwrites = {
UpgradeType: upgradeType,
UpgradeFingerprint: JSON.stringify({
compat: payload.ItemType,
buffs
} satisfies IInnateDamageFingerprint)
};
}
addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges);
}
inventoryChanges.CrewShipRawSalvage = [
{
ItemType: payload.ItemType,
ItemCount: -1
}
];
addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipIdentifySalvageRequest {
ItemType: string;
}

View File

@ -1,48 +0,0 @@
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

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

View File

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

View File

@ -1,4 +1,3 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
@ -36,10 +35,10 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
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);
processContribution(deco, meta, platinumDonated);
} else {
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(guild, component, meta, platinumDonated);
processContribution(component, meta, platinumDonated);
const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
if (entry) {
@ -47,25 +46,16 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
}
}
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()]);
await guild.save();
await inventory.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 processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => {
const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice);
const fullDurationSeconds = meta.time;
const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost;
component.CompletionTime = new Date(

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,10 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus";
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -30,15 +31,32 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
}
addMiscItems(inventory, miscItemChanges);
let affiliationMod;
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
if (gainedStanding && syndicateTag) {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
}
const syndicateMeta = ExportSyndicates[syndicateTag];
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
syndicate.Standing += gainedStanding;
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: miscItemChanges
},
SyndicateTag: syndicateTag,
StandingChange: affiliationMod?.Standing || 0
StandingChange: gainedStanding
});
};

View File

@ -18,15 +18,17 @@ export const focusController: RequestHandler = async (req, res) => {
case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(accountId);
const item = inventory[request.Category].id(request.WeaponId);
if (item) {
item.FocusLens = request.LensType;
addMiscItems(inventory, [
{
ItemType: request.LensType,
ItemCount: -1
} satisfies IMiscItem
]);
for (const item of inventory[request.Category]) {
if (item._id.toString() == request.WeaponId) {
item.FocusLens = request.LensType;
addMiscItems(inventory, [
{
ItemType: request.LensType,
ItemCount: -1
} satisfies IMiscItem
]);
break;
}
}
await inventory.save();
res.json({
@ -104,14 +106,13 @@ export const focusController: RequestHandler = async (req, res) => {
}
case FocusOperation.SentTrainingAmplifier: {
const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
const parts: string[] = [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
];
const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
]
});
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);

View File

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

View File

@ -1,25 +1,7 @@
import { Alliance, Guild } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const getAllianceController: RequestHandler = async (req, res) => {
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();
const getAllianceController: RequestHandler = (_req, res) => {
res.sendStatus(200);
};
/*interface IGetAllianceRequest {
memberCount: number;
clanLeaderName: string;
clanLeaderId: string;
}*/
export { getAllianceController };

View File

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

View File

@ -5,11 +5,11 @@ import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
export const getGuildController: RequestHandler = async (req, res) => {
const getGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId);
const guild = await Guild.findOne({ _id: inventory.GuildId });
if (guild) {
// Handle guilds created before we added discriminators
if (guild.Name.indexOf("#") == -1) {
@ -28,5 +28,7 @@ export const getGuildController: RequestHandler = async (req, res) => {
return;
}
}
res.end();
res.sendStatus(200);
};
export { getGuildController };

View File

@ -6,7 +6,7 @@ import { getDojoClient } from "@/src/services/guildService";
export const getGuildDojoController: RequestHandler = async (req, res) => {
const guildId = req.query.guildId as string;
const guild = await Guild.findById(guildId);
const guild = await Guild.findOne({ _id: guildId });
if (!guild) {
res.status(404).end();
return;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,92 +0,0 @@
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

@ -2,25 +2,36 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { WeaponTypeInternal } from "@/src/services/itemDataService";
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")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards"
];
interface IGildWeaponRequest {
ItemName: string;
Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity;
ItemId: string;
Category: TEquipmentKey;
Category: WeaponTypeInternal | "Hoverboards";
}
export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
data.ItemId = String(req.query.ItemId);
data.Category = req.query.Category as TEquipmentKey;
if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) {
throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
}
data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId);
const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
@ -31,10 +42,8 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const weapon = inventory[data.Category][weaponIndex];
weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED;
if (data.Recipe != "webui") {
weapon.ItemName = data.ItemName;
weapon.XP = 0;
}
weapon.ItemName = data.ItemName;
weapon.XP = 0;
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
weapon.Polarity = [
{
@ -47,24 +56,21 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
const affiliationMods = [];
if (data.Recipe != "webui") {
const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType,
ItemCount: ingredient.ItemCount * -1
}));
addMiscItems(inventory, inventoryChanges.MiscItems);
if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value;
affiliationMods.push({
Tag: recipe.syndicateStandingChange.tag,
Standing: recipe.syndicateStandingChange.value
});
}
if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value;
affiliationMods.push({
Tag: recipe.syndicateStandingChange.tag,
Standing: recipe.syndicateStandingChange.value
});
}
await inventory.save();

View File

@ -2,8 +2,8 @@ import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { IGroup } from "@/src/types/loginTypes";
import { giveKeyChainItem } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
@ -15,3 +15,9 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req,
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 { getAccountIdForRequest } from "@/src/services/loginService";
import { giveKeyChainMessage } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express";
export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +1,21 @@
import { RequestHandler } from "express";
import { Inbox } from "@/src/models/inboxModel";
import {
createMessage,
createNewEventMessages,
deleteAllMessagesRead,
deleteMessageRead,
getAllMessagesSorted,
getMessage
} from "@/src/services/inboxService";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addItems, getInventory } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { ExportFlavour, ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
import { ExportGear } from "warframe-public-export-plus";
export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
const account = await getAccountForRequest(req);
const accountId = account._id.toString();
const accountId = await getAccountIdForRequest(req);
if (deleteId) {
if (deleteId === "DeleteAllRead") {
@ -33,12 +29,12 @@ export const inboxController: RequestHandler = async (req, res) => {
} else if (messageId) {
const message = await getMessage(messageId as string);
message.r = true;
await message.save();
const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt;
const attachmentItems = message.attVisualOnly ? undefined : message.att;
const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
if (!attachmentItems && !attachmentCountedItems) {
await message.save();
if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
res.status(200).end();
return;
}
@ -49,7 +45,7 @@ export const inboxController: RequestHandler = async (req, res) => {
await addItems(
inventory,
attachmentItems.map(attItem => ({
ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem,
ItemType: attItem,
ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})),
inventoryChanges
@ -58,43 +54,9 @@ export const inboxController: RequestHandler = async (req, res) => {
if (attachmentCountedItems) {
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 message.save();
res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) {
await createNewEventMessages(req);

View File

@ -6,21 +6,20 @@ import { IOid } from "@/src/types/commonTypes";
import {
IConsumedSuit,
IHelminthFoodRecord,
IInfestedFoundryClient,
IInfestedFoundryDatabase,
IInventoryClient,
IMiscItem,
InventorySlot
InventorySlot,
ITypeCount
} from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc } from "warframe-public-export-plus";
import { ExportMisc, ExportRecipes } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService";
import {
addInfestedFoundryXP,
applyCheatsToInfestedFoundry,
handleSubsumeCompletion
} from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -29,7 +28,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
}
@ -57,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!;
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = [];
if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
@ -384,11 +383,116 @@ interface IHelminthFeedRequest {
}[];
}
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
const recipeChanges: ITypeCount[] = [];
infestedFoundry.XP ??= 0;
const prevXP = infestedFoundry.XP;
infestedFoundry.XP += delta;
if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 3;
}
if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
ItemCount: 1
});
}
if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
}
if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 10;
}
if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
ItemCount: 1
});
}
if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
}
if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 20;
}
if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
}
if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
infestedFoundry.Slots = 1;
}
if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
ItemCount: 1
});
}
if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
ItemCount: 1
});
}
if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
ItemCount: 1
});
}
if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
ItemCount: 1
});
}
if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
ItemCount: 1
});
}
return recipeChanges;
};
interface IHelminthSubsumeRequest {
SuitId: IOid;
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 {
OfferingsIndex: number;
SuitTypes: string[];

View File

@ -13,17 +13,9 @@ import {
ExportResources,
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import {
addMiscItems,
allDailyAffiliationKeys,
cleanupInventory,
createLibraryDailyTask,
generateRewardSeed
} from "@/src/services/inventoryService";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController";
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
export const inventoryController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
@ -41,8 +33,6 @@ export const inventoryController: RequestHandler = async (request, response) =>
inventory[key] = 16000 + inventory.PlayerLevel * 500;
}
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.TradesRemaining = inventory.PlayerLevel;
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
@ -60,11 +50,9 @@ export const inventoryController: RequestHandler = async (request, response) =>
if (numArgonCrystals == 0) {
break;
}
const numStableArgonCrystals = Math.min(
numArgonCrystals,
const numStableArgonCrystals =
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0
);
?.ItemCount ?? 0;
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
@ -86,10 +74,8 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
}
cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
//await inventory.save();
await inventory.save();
}
if (
@ -98,20 +84,9 @@ export const inventoryController: RequestHandler = async (request, response) =>
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) {
handleSubsumeCompletion(inventory);
//await inventory.save();
await inventory.save();
}
if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save();
response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
};
@ -169,7 +144,7 @@ export const getInventoryResponse = async (
inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
}
}
}
@ -222,8 +197,7 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = [];
// 12 is needed for necramechs. 15 is needed for plexus/crewshipharness.
for (let i = 0; i != 15; ++i) {
for (let i = 0; i != 12; ++i) {
Polarity.push({
Slot: i,
Value: ArtifactPolarity.Any
@ -278,16 +252,12 @@ export const getInventoryResponse = async (
}
}
if (config.noDailyFocusLimit) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
}
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
inventoryResponse.LastInventorySync = undefined;
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
@ -295,7 +265,7 @@ export const getInventoryResponse = async (
return inventoryResponse;
};
const addString = (arr: string[], str: string): void => {
export const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) {
arr.push(str);
}
@ -325,3 +295,13 @@ const resourceGetParent = (resourceName: string): string | undefined => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName;
};
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
export const catBreadHash = (name: string): number => {
let hash = 2166136261;
for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
hash = (hash * 16777619) & 0x7fffffff;
}
return hash;
};

View File

@ -21,14 +21,10 @@ export const loginController: RequestHandler = async (request, response) => {
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
if (
!account &&
((config.autoCreateAccount && loginRequest.ClientType != "webui") ||
loginRequest.ClientType == "webui-register")
) {
if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
let name = nameFromEmail;
if (await isNameTaken(name)) {
let suffix = 0;
do {
@ -41,7 +37,7 @@ export const loginController: RequestHandler = async (request, response) => {
password: loginRequest.password,
DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
ClientType: loginRequest.ClientType,
CrossPlatformAllowed: true,
ForceLogoutVersion: 0,
ConsentNeeded: false,
@ -58,17 +54,8 @@ export const loginController: RequestHandler = async (request, response) => {
}
}
if (!account) {
response.status(400).json({ error: "unknown user" });
return;
}
if (loginRequest.ClientType == "webui-register") {
response.status(400).json({ error: "account already exists" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) {
//email not found or incorrect password
if (!account || !isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" });
return;
}

View File

@ -1,43 +1,31 @@
import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService";
import {
claimLoginReward,
getRandomLoginRewards,
ILoginRewardsReponse,
isLoginRewardAChoice,
setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService";
import { claimLoginReward, getRandomLoginRewards, ILoginRewardsReponse } from "@/src/services/loginRewardService";
import { getInventory } from "@/src/services/inventoryService";
export const loginRewardsController: RequestHandler = async (req, res) => {
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);
res.end();
return;
}
account.LoginDays += 1;
account.LastLoginRewardDate = today;
await account.save();
const inventory = await getInventory(account._id.toString());
const randomRewards = getRandomLoginRewards(account, inventory);
const isMilestoneDay = account.LoginDays == 5 || account.LoginDays % 50 == 0;
const response: ILoginRewardsReponse = {
DailyTributeInfo: {
Rewards: randomRewards,
IsMilestoneDay: isMilestoneDay,
IsChooseRewardSet: randomRewards.length != 1,
LoginDays: account.LoginDays,
NextMilestoneReward: "",
NextMilestoneDay: nextMilestoneDay,
//NextMilestoneReward: "",
NextMilestoneDay: account.LoginDays < 5 ? 5 : (Math.trunc(account.LoginDays / 50) + 1) * 50,
HasChosenReward: false
},
LastLoginRewardDate: today
@ -47,9 +35,6 @@ export const loginRewardsController: RequestHandler = async (req, res) => {
response.DailyTributeInfo.ChosenReward = randomRewards[0];
response.DailyTributeInfo.NewInventory = await claimLoginReward(inventory, randomRewards[0]);
await inventory.save();
setAccountGotLoginRewardToday(account);
await account.save();
}
res.json(response);
};

View File

@ -1,9 +1,5 @@
import { getInventory } from "@/src/services/inventoryService";
import {
claimLoginReward,
getRandomLoginRewards,
setAccountGotLoginRewardToday
} from "@/src/services/loginRewardService";
import { claimLoginReward, getRandomLoginRewards } from "@/src/services/loginRewardService";
import { getAccountForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
@ -35,10 +31,6 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
inventoryChanges = await claimLoginReward(inventory, chosenReward);
}
await inventory.save();
setAccountGotLoginRewardToday(account);
await account.save();
res.json({
DailyTributeInfo: {
NewInventory: inventoryChanges,

View File

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

View File

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

View File

@ -3,10 +3,9 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { generateRewardSeed, getInventory } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController";
import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
/*
**** INPUT ****
@ -48,22 +47,16 @@ import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
- [ ] FpsSamples
*/
//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> => {
const accountId = await getAccountIdForRequest(req);
const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString());
logger.debug("mission report:", missionReport);
const inventory = await getInventory(accountId);
const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if (
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
inventory.RewardSeed = generateRewardSeed();
if (missionReport.MissionStatus !== "GS_SUCCESS") {
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({
@ -73,16 +66,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return;
}
const {
MissionRewards,
inventoryChanges,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport);
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
@ -93,11 +78,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
MissionRewards,
...credits,
...inventoryUpdates,
//FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed.
SyndicateXPItemReward,
AffiliationMods,
ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse);
FusionPoints: inventoryChanges?.FusionPoints
});
};
/*

View File

@ -8,22 +8,15 @@ import {
addMiscItems,
applyDefaultUpgrades,
occupySlot,
productCategoryToInventoryBin,
combineInventoryChanges,
addSpecialItem
productCategoryToInventoryBin
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
interface IModularCraftRequest {
WeaponType: string;
Parts: string[];
isWebUi?: boolean;
}
export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
@ -35,158 +28,34 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId);
let defaultUpgrades: IDefaultUpgrade[] | undefined;
const defaultOverwrites: Partial<IEquipmentDatabase> = {
ModularParts: data.Parts
const defaultUpgrades = getDefaultUpgrades(data.Parts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs }),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, false)
};
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];
if (!traits) {
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]];
const meta = ExportSentinels[data.WeaponType];
for (const specialItem of meta.exalted!) {
addSpecialItem(inventory, specialItem, inventoryChanges);
}
defaultUpgrades = meta.defaultUpgrades;
} else {
defaultUpgrades = getDefaultUpgrades(data.Parts);
}
if (category == "MoaPets") {
const weapon = ExportSentinels[data.WeaponType].defaultWeapon;
if (weapon) {
const category = ExportWeapons[weapon].productCategory;
addEquipment(inventory, category, weapon, undefined, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
}
}
defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades);
addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
// Remove credits & parts
const miscItemChanges = [];
let currencyChanges = {};
if (!data.isWebUi) {
for (const part of data.Parts) {
miscItemChanges.push({
ItemType: part,
ItemCount: -1
});
}
currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" ||
category == "MoaPets" ||
category == "LongGuns" ||
category == "Pistols" ||
category == "KubrowPets"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
for (const part of data.Parts) {
miscItemChanges.push({
ItemType: part,
ItemCount: -1
});
}
const currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" || category == "MoaPets" || category == "LongGuns" || category == "Pistols"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
await inventory.save();
// Tell client what we did
res.json({
InventoryChanges: {

View File

@ -21,11 +21,8 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {};
for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if (
data.partType &&
data.premiumPrice &&
!data.excludeFromCodex // exclude pvp variants
) {
if (data.partType) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName);
}
@ -45,18 +42,24 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(inventory, category, weaponInfo.ItemType, {
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
ModularParts: weaponInfo.ModularParts,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}),
...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)
};
@ -141,10 +144,14 @@ const getModularWeaponSale = (
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType]));
let partsCost = 0;
for (const part of parts) {
partsCost += ExportWeapons[part].premiumPrice!;
const meta = ExportWeapons[part];
if (!meta.premiumPrice) {
throw new Error(`no premium price for ${part}`);
}
partsCost += meta.premiumPrice;
}
return {
Name: name,

View File

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

View File

@ -1,31 +1,10 @@
import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getKnifeUpgrade,
getNemesisPasscode,
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
} from "@/src/helpers/nemesisHelpers";
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
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 { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IInnateDamageFingerprint,
IInventoryClient,
INemesisClient,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
@ -39,7 +18,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
// Update destination damage type if desired
// Upgrade destination damage type if desireed
if (body.UseSourceDmgType) {
destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
}
@ -48,7 +27,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
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) {
if (newDamage >= 0.58) {
newDamage = 0.6;
}
destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
@ -63,14 +42,13 @@ export const nemesisController: RequestHandler = async (req, res) => {
await inventory.save();
res.json({
InventoryChanges: {
[body.Category]: [destWeapon.toJSON()],
RemovedIdItems: [{ ItemId: body.SourceWeapon }]
[body.Category]: [destWeapon.toJSON()]
}
});
} 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!);
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) {
@ -87,83 +65,6 @@ export const nemesisController: RequestHandler = async (req, res) => {
}
}
res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
// Add to GuessHistory
const result1 = passcode == guess[0] ? 0 : 1;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
);
// Increase antivirus if correct antivirus mod is installed
const response: IKnifeResponse = {};
if (result1 == 0 || result2 == 0 || result3 == 0) {
let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
}
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
}
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
await inventory.save();
res.json(response);
} else {
const passcode = getNemesisPasscode(inventory.Nemesis!);
if (passcode[body.position] != body.guess) {
res.end();
} else {
inventory.Nemesis!.Rank += 1;
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
await inventory.save();
res.json({ RankIncrease: 1 });
}
}
} else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body
const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
@ -171,7 +72,18 @@ export const nemesisController: RequestHandler = async (req, res) => {
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
const weapons = getWeaponsForManifest(body.target.manifest);
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 {
@ -213,38 +125,6 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({
target: inventory.toJSON().Nemesis
});
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [
{
Node: showdownNodes[inventory.Nemesis!.Faction],
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const response: IKnifeResponse & { target: INemesisClient } = {
target: inventory.toJSON<IInventoryClient>().Nemesis!
};
// Consume charge of the correct requiem mod(s)
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
for (const modType of modTypes) {
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
await inventory.save();
res.json(response);
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
@ -292,23 +172,38 @@ interface INemesisPrespawnCheckRequest {
potency?: number[];
}
interface INemesisRequiemRequest {
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
position: number; // grn/crp: 0-2 | coda: 0
// knife field provided for coda only
knife?: IKnife;
}
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"
];
// interface INemesisWeakenRequest {
// target: INemesisClient;
// knife: IKnife;
// }
interface IKnife {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
}
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

@ -1,19 +1,10 @@
import {
getDojoClient,
getGuildForRequestEx,
getVaultMiscItemCount,
hasAccessToDojo,
hasGuildPermission,
processDojoBuildMaterialsGathered,
scaleRequiredCount
} from "@/src/services/guildService";
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 { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
import { ExportDojoRecipes } from "warframe-public-export-plus";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -33,76 +24,17 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
}
component.Decos ??= [];
if (request.MoveId) {
const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
deco.Pos = request.Pos;
deco.Rot = request.Rot;
deco.Scale = request.Scale;
} else {
const deco =
component.Decos[
component.Decos.push({
_id: new Types.ObjectId(),
Type: request.Type,
Pos: request.Pos,
Rot: request.Rot,
Scale: request.Scale,
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 (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
deco.CompletionTime = new Date();
if (meta) {
processDojoBuildMaterialsGathered(guild, meta);
}
} 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);
component.Decos.push({
_id: new Types.ObjectId(),
Type: request.Type,
Pos: request.Pos,
Rot: request.Rot,
Name: request.Name
});
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);
}
}
}
}
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type);
if (meta && meta.capacityCost) {
component.DecoCapacity -= meta.capacityCost;
}
await guild.save();
@ -115,10 +47,5 @@ interface IPlaceDecoInComponentRequest {
Type: string;
Pos: number[];
Rot: number[];
Scale?: number;
Name?: string;
Sockets?: number;
MoveId?: string;
ShipDeco?: boolean;
VaultDeco?: boolean;
}

View File

@ -1,9 +0,0 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const playedParkourTutorialController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true });
res.end();
};

View File

@ -1,75 +0,0 @@
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

@ -16,7 +16,7 @@ export const queueDojoComponentDestructionController: RequestHandler = async (re
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
Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000)
);
await guild.save();

View File

@ -1,34 +0,0 @@
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

@ -1,27 +0,0 @@
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,
payload.recipeName == "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" ? 25000 : 0,
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" | "webui";
petId: string;
}

View File

@ -1,38 +0,0 @@
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

@ -1,8 +1,6 @@
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 { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
@ -19,61 +17,45 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
}
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 });
if (guildMember.status == 0) {
const inventory = await getInventory(payload.userId);
inventory.GuildId = undefined;
guild.RosterActivity ??= [];
if (isKick) {
const kickee = (await Account.findById(payload.userId))!;
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 12,
details: getSuffixedName(kickee) + "," + getSuffixedName(account)
});
// Remove clan key or blueprint from kicked member
const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey");
if (itemIndex != -1) {
inventory.LevelKeys.splice(itemIndex, 1);
} else {
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 7,
details: getSuffixedName(account)
});
const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint");
if (recipeIndex != -1) {
inventory.Recipes.splice(itemIndex, 1);
}
}
await guild.save();
await inventory.save();
// TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think)
} else if (guildMember.status == 2) {
// TODO: Maybe the inbox message for the sent invite should be deleted?
}
await GuildMember.deleteOne({ _id: guildMember._id });
guild.RosterActivity ??= [];
if (isKick) {
const kickee = (await Account.findOne({ _id: 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,

View File

@ -1,21 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const removeIgnoredUserController: RequestHandler = async (req, res) => {
const accountId = await getAccountForRequest(req);
const data = getJSONfromString<IRemoveIgnoredUserRequest>(String(req.body));
const ignoreeAccount = await Account.findOne(
{ DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
"_id"
);
if (ignoreeAccount) {
await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id });
}
res.end();
};
interface IRemoveIgnoredUserRequest {
playerName: string;
}

View File

@ -1,33 +0,0 @@
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

@ -1,41 +1,53 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
if ("YearIteration" in request) {
const inventory = await getInventory(accountId, "DialogueHistory");
inventory.DialogueHistory ??= {};
inventory.DialogueHistory.YearIteration = request.YearIteration;
const inventory = await getInventory(accountId);
if (inventory.DialogueHistory) {
inventory.DialogueHistory.YearIteration = request.YearIteration;
} else {
inventory.DialogueHistory = { YearIteration: request.YearIteration };
}
await inventory.save();
res.end();
} else {
const inventory = await getInventory(accountId);
const inventoryChanges: IInventoryChanges = {};
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
inventory.DialogueHistory ??= {};
if (!inventory.DialogueHistory) {
throw new Error("bad inventory state");
}
if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
}
inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName);
let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory.Dialogues[
inventory.DialogueHistory.Dialogues.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: request.DialogueName
}) - 1
];
}
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues;
//dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
if (bool == "LizzieShawzin") {
await addEmailItem(
inventory,
"/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
inventoryChanges
);
}
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
@ -43,38 +55,14 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
dialogue.Booleans.splice(index, 1);
}
}
for (const info of request.OtherDialogueInfos) {
const otherDialogue = getDialogue(inventory, info.Dialogue);
if (info.Tag != "") {
otherDialogue.QueuedDialogues.push(info.Tag);
}
otherDialogue.Chemistry += info.Value; // unsure
}
if (request.Data) {
dialogue.Completed.push(request.Data);
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
} else if (request.Gift) {
const inventoryChanges = updateCurrency(inventory, request.Gift.Cost, false);
const gift = dialogue.Gifts.find(x => x.Item == request.Gift!.Item);
if (gift) {
gift.GiftedQuantity += 1;
} else {
dialogue.Gifts.push({ Item: request.Gift.Item, GiftedQuantity: 1 });
}
dialogue.AvailableGiftDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
} else {
res.end();
}
dialogue.Completed.push(request.Data);
const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: [],
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
}
};
@ -89,17 +77,11 @@ interface SaveCompletedDialogueRequest {
Rank: number;
Chemistry: number;
CompletionType: number;
QueuedDialogues: string[];
Gift?: {
Item: string;
GainedChemistry: number;
Cost: number;
GiftedQuantity: number;
};
QueuedDialogues: string[]; // unsure
Booleans: string[];
ResetBooleans: string[];
Data?: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[];
Data: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[]; // unsure
}
interface IOtherDialogueInfo {
@ -107,26 +89,3 @@ interface IOtherDialogueInfo {
Tag: string;
Value: number;
}
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};

View File

@ -0,0 +1,32 @@
import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
export const saveLoadoutController: RequestHandler = async (req, res) => {
//validate here
const accountId = await getAccountIdForRequest(req);
try {
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {
res.send(newLoadoutId);
}
res.status(200).end();
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`error in saveLoadoutController: ${error.message}`);
res.status(400).json({ error: error.message });
} else {
res.status(400).json({ error: "unknown error" });
}
}
};

View File

@ -1,21 +0,0 @@
import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {
res.send(newLoadoutId);
}
res.end();
};

View File

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

View File

@ -1,25 +0,0 @@
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

@ -6,71 +6,20 @@ import {
addRecipes,
addMiscItems,
addConsumables,
freeUpSlot,
combineInventoryChanges,
addCrewShipRawSalvage,
addFusionPoints
freeUpSlot
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req);
const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
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 as keyof TInventoryDatabaseDocument);
}
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 || payload.Items.MoaPets) {
requiredFields.add(InventorySlot.SENTINELS);
}
if (payload.Items.OperatorAmps) {
requiredFields.add(InventorySlot.AMPS);
}
if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS);
}
if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
requiredFields.add("CrewShipRawSalvage");
if (payload.Items.CrewShipWeapons) {
requiredFields.add("CrewShipSalvagedWeapons");
}
if (payload.Items.CrewShipWeaponSkins) {
requiredFields.add("CrewShipSalvagedWeaponSkins");
}
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
const inventory = await getInventory(accountId);
// Give currency
if (payload.SellCurrency == "SC_RegularCredits") {
inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") {
addFusionPoints(inventory, payload.SellPrice);
inventory.FusionPoints += payload.SellPrice;
} else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [
{
@ -85,14 +34,10 @@ export const sellController: RequestHandler = async (req, res) => {
ItemCount: payload.SellPrice
}
]);
} else if (payload.SellCurrency == "SC_Resources") {
// Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
} else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
}
const inventoryChanges: IInventoryChanges = {};
// Remove item(s)
if (payload.Items.Suits) {
payload.Items.Suits.forEach(sellItem => {
@ -148,12 +93,6 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.MoaPets) {
payload.Items.MoaPets.forEach(sellItem => {
inventory.MoaPets.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String });
@ -171,56 +110,6 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.Drones.pull({ _id: sellItem.String });
});
}
if (payload.Items.CrewShipWeapons) {
payload.Items.CrewShipWeapons.forEach(sellItem => {
if (sellItem.String[0] == "/") {
addCrewShipRawSalvage(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
} else {
const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges);
}
inventory.CrewShipWeapons.splice(index, 1);
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
} else {
inventory.CrewShipSalvagedWeapons.pull({ _id: sellItem.String });
}
}
});
}
if (payload.Items.CrewShipWeaponSkins) {
payload.Items.CrewShipWeaponSkins.forEach(sellItem => {
if (sellItem.String[0] == "/") {
addCrewShipRawSalvage(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
} else {
const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
refundPartialBuildCosts(
inventory,
inventory.CrewShipWeaponSkins[index].ItemType,
inventoryChanges
);
}
inventory.CrewShipWeaponSkins.splice(index, 1);
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
} else {
inventory.CrewShipSalvagedWeaponSkins.pull({ _id: sellItem.String });
}
}
});
}
if (payload.Items.Consumables) {
const consumablesChanges = [];
for (const sellItem of payload.Items.Consumables) {
@ -267,9 +156,7 @@ export const sellController: RequestHandler = async (req, res) => {
}
await inventory.save();
res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
});
res.json({});
};
interface ISellRequest {
@ -287,12 +174,9 @@ interface ISellRequest {
SpaceMelee?: ISellItem[];
Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[];
OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[];
Drones?: ISellItem[];
CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[];
};
SellPrice: number;
SellCurrency:
@ -309,33 +193,3 @@ interface ISellItem {
String: string; // oid or uniqueName
Count: number;
}
const refundPartialBuildCosts = (
inventory: TInventoryDatabaseDocument,
itemType: string,
inventoryChanges: IInventoryChanges
): void => {
// House versions
const research = Object.values(ExportDojoRecipes.research).find(x => x.resultType == itemType);
if (research) {
const miscItemChanges = research.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: Math.trunc(x.ItemCount * 0.8)
}));
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
return;
}
// Sigma versions
const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType);
if (recipe) {
const miscItemChanges = recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: Math.trunc(x.ItemCount * 0.8)
}));
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
return;
}
};

View File

@ -1,31 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createMessage } from "@/src/services/inboxService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const sendMsgToInBoxController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<ISendMsgToInBoxRequest>(String(req.body));
await createMessage(accountId, [
{
sub: data.title,
msg: data.message,
sndr: data.sender ?? "/Lotus/Language/Bosses/Ordis",
icon: data.senderIcon,
highPriority: data.highPriority,
transmission: data.transmission,
att: data.attachments
}
]);
res.end();
};
interface ISendMsgToInBoxRequest {
title: string;
message: string;
sender?: string;
senderIcon?: string;
highPriority?: boolean;
transmission?: string;
attachments?: string[];
}

View File

@ -1,38 +0,0 @@
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

@ -1,34 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
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 setDojoComponentColorsController: 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 data = getJSONfromString<ISetDojoComponentColorsRequest>(String(req.body));
const component = guild.DojoComponents.id(data.ComponentId)!;
//const deco = component.Decos!.find(x => x._id.equals(data.DecoId))!;
//deco.Pending = true;
//component.PaintBot = new Types.ObjectId(data.DecoId);
if ("lights" in req.query) {
component.PendingLights = data.Colours;
} else {
component.PendingColors = data.Colours;
}
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface ISetDojoComponentColorsRequest {
ComponentId: string;
DecoId: string;
Colours: number[];
}

View File

@ -1,25 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
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 setDojoComponentSettingsController: 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 component = guild.DojoComponents.id(req.query.componentId)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings;
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface ISetDojoComponentSettingsRequest {
Settings: string;
}

View File

@ -1,59 +1,35 @@
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermissionEx } from "@/src/services/guildService";
import { Guild } from "@/src/models/guildModel";
import { hasGuildPermission } 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 { GuildPermission } 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 guild = (await Guild.findOne({ _id: inventory.GuildId! }))!;
if (!(await hasGuildPermission(guild, account._id, GuildPermission.Herald))) {
res.status(400).json("Invalid permission");
return;
}
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;
if (IsLongMOTD) {
if (MOTD) {
guild.LongMOTD = {
message: MOTD,
authorName: getSuffixedName(account)
};
} else {
alliance.MOTD = motd;
guild.LongMOTD = undefined;
}
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();
guild.MOTD = MOTD ?? "";
}
await guild.save();
res.json({ IsLongMOTD, MOTD });
};

View File

@ -1,21 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IHubNpcCustomization } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const setHubNpcCustomizationsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "HubNpcCustomizations");
const upload = getJSONfromString<IHubNpcCustomization>(String(req.body));
inventory.HubNpcCustomizations ??= [];
const cust = inventory.HubNpcCustomizations.find(x => x.Tag == upload.Tag);
if (cust) {
cust.Colors = upload.Colors;
cust.Pattern = upload.Pattern;
} else {
inventory.HubNpcCustomizations.push(upload);
}
await inventory.save();
res.end();
};

View File

@ -1,5 +1,5 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
@ -7,17 +7,5 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
await handleSetPlacedDecoInfo(accountId, payload);
res.json({
DecoId: payload.DecoId,
IsPicture: true,
PictureFrameInfo: payload.PictureFrameInfo,
BootLocation: payload.BootLocation
} satisfies ISetPlacedDecoInfoResponse);
res.end();
};
interface ISetPlacedDecoInfoResponse {
DecoId: string;
IsPicture: boolean;
PictureFrameInfo?: IPictureFrameInfo;
BootLocation?: string;
}

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