Compare commits

..

2 Commits
main ... main

Author SHA1 Message Date
3809d6678c Fixed formatting 2025-05-04 18:26:35 +02:00
d117c93ef7 Fix acquiring blueprints as rewards
Fixes the acquisition of blueprints as rewards, such as those rewarded by the Junctions.
2025-05-04 18:26:34 +02:00
122 changed files with 3553 additions and 6141 deletions

View File

@ -10,8 +10,6 @@ jobs:
uses: actions/checkout@v4.1.2 uses: actions/checkout@v4.1.2
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with:
node-version: ">=20.6.0"
- run: npm ci - run: npm ci
- run: cp config.json.example config.json - run: cp config.json.example config.json
- run: npm run verify - run: npm run verify

View File

@ -14,8 +14,6 @@ ENV APP_INFINITE_PLATINUM=false
ENV APP_INFINITE_ENDO=false ENV APP_INFINITE_ENDO=false
ENV APP_INFINITE_REGAL_AYA=false ENV APP_INFINITE_REGAL_AYA=false
ENV APP_INFINITE_HELMINTH_MATERIALS=false ENV APP_INFINITE_HELMINTH_MATERIALS=false
ENV APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS=false
ENV APP_DONT_SUBTRACT_VOIDTRACES=false
ENV APP_DONT_SUBTRACT_CONSUMABLES=false ENV APP_DONT_SUBTRACT_CONSUMABLES=false
ENV APP_UNLOCK_ALL_SHIP_FEATURES=false ENV APP_UNLOCK_ALL_SHIP_FEATURES=false
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=false
@ -32,8 +30,6 @@ ENV APP_NO_MASTERY_RANK_UP_COOLDOWN=false
ENV APP_NO_VENDOR_PURCHASE_LIMITS=true ENV APP_NO_VENDOR_PURCHASE_LIMITS=true
ENV APP_NO_DEATH_MARKS=false ENV APP_NO_DEATH_MARKS=false
ENV APP_NO_KIM_COOLDOWNS=false ENV APP_NO_KIM_COOLDOWNS=false
ENV APP_SYNDICATE_MISSIONS_REPEATABLE=false
ENV APP_INSTANT_FINISH_RIVEN_CHALLENGE=false
ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false ENV APP_INSTANT_RESOURCE_EXTRACTOR_DRONES=false
ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false ENV APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE=false
ENV APP_SKIP_CLAN_KEY_CRAFTING=false ENV APP_SKIP_CLAN_KEY_CRAFTING=false

View File

@ -14,22 +14,4 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift. - `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.
- `worldState.vallisOverride` can be set to `warm` or `cold` to lock the temperature on Orb Vallis.
- `worldState.nightwaveOverride` will lock the nightwave season, assuming the client is new enough for it. Valid values:
- `RadioLegionIntermission13Syndicate` for Nora's Mix Vol. 9
- `RadioLegionIntermission12Syndicate` for Nora's Mix Vol. 8
- `RadioLegionIntermission11Syndicate` for Nora's Mix Vol. 7
- `RadioLegionIntermission10Syndicate` for Nora's Mix Vol. 6
- `RadioLegionIntermission9Syndicate` for Nora's Mix Vol. 5
- `RadioLegionIntermission8Syndicate` for Nora's Mix Vol. 4
- `RadioLegionIntermission7Syndicate` for Nora's Mix Vol. 3
- `RadioLegionIntermission6Syndicate` for Nora's Mix Vol. 2
- `RadioLegionIntermission5Syndicate` for Nora's Mix Vol. 1
- `RadioLegionIntermission4Syndicate` for Nora's Choice
- `RadioLegionIntermission3Syndicate` for Intermission III
- `RadioLegion3Syndicate` for Glassmaker
- `RadioLegionIntermission2Syndicate` for Intermission II
- `RadioLegion2Syndicate` for The Emissary
- `RadioLegionIntermissionSyndicate` for Intermission I
- `RadioLegionSyndicate` for The Wolf of Saturn Six

View File

@ -3,7 +3,7 @@
echo Updating SpaceNinjaServer... echo Updating SpaceNinjaServer...
git fetch --prune git fetch --prune
git stash git stash
git checkout -f origin/main git reset --hard origin/main
if exist static\data\0\ ( if exist static\data\0\ (
echo Updating stripped assets... echo Updating stripped assets...

View File

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

View File

@ -19,8 +19,6 @@
"infiniteEndo": false, "infiniteEndo": false,
"infiniteRegalAya": false, "infiniteRegalAya": false,
"infiniteHelminthMaterials": false, "infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false,
"dontSubtractVoidTraces": false,
"dontSubtractConsumables": false, "dontSubtractConsumables": false,
"unlockAllShipFeatures": false, "unlockAllShipFeatures": false,
"unlockAllShipDecorations": false, "unlockAllShipDecorations": false,
@ -38,8 +36,6 @@
"noVendorPurchaseLimits": true, "noVendorPurchaseLimits": true,
"noDeathMarks": false, "noDeathMarks": false,
"noKimCooldowns": false, "noKimCooldowns": false,
"syndicateMissionsRepeatable": false,
"instantFinishRivenChallenge": false,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false, "noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false, "skipClanKeyCrafting": false,
@ -50,17 +46,11 @@
"noDojoResearchTime": false, "noDojoResearchTime": false,
"fastClanAscension": false, "fastClanAscension": false,
"spoofMasteryRank": -1, "spoofMasteryRank": -1,
"nightwaveStandingMultiplier": 1,
"worldState": { "worldState": {
"creditBoost": false, "creditBoost": false,
"affinityBoost": false, "affinityBoost": false,
"resourceBoost": false, "resourceBoost": false,
"starDays": true, "starDays": true,
"eidolonOverride": "", "lockTime": 0
"vallisOverride": "",
"nightwaveOverride": ""
},
"dev": {
"keepVendorsExpired": false
} }
} }

View File

@ -21,8 +21,6 @@ services:
# APP_INFINITE_ENDO: false # APP_INFINITE_ENDO: false
# APP_INFINITE_REGAL_AYA: false # APP_INFINITE_REGAL_AYA: false
# APP_INFINITE_HELMINTH_MATERIALS: false # APP_INFINITE_HELMINTH_MATERIALS: false
# APP_CLAIMING_BLUEPRINT_REFUNDS_INGREDIENTS: false
# APP_DONT_SUBTRACT_VOIDTRACES: false
# APP_DONT_SUBTRACT_CONSUMABLES: false # APP_DONT_SUBTRACT_CONSUMABLES: false
# APP_UNLOCK_ALL_SHIP_FEATURES: false # APP_UNLOCK_ALL_SHIP_FEATURES: false
# APP_UNLOCK_ALL_SHIP_DECORATIONS: false # APP_UNLOCK_ALL_SHIP_DECORATIONS: false
@ -39,8 +37,6 @@ services:
# APP_NO_VENDOR_PURCHASE_LIMITS: true # APP_NO_VENDOR_PURCHASE_LIMITS: true
# APP_NO_DEATH_MARKS: false # APP_NO_DEATH_MARKS: false
# APP_NO_KIM_COOLDOWNS: false # APP_NO_KIM_COOLDOWNS: false
# APP_SYNDICATE_MISSIONS_REPEATABLE: false
# APP_INSTANT_FINISH_RIVEN_CHALLENGE: false
# APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false # APP_INSTANT_RESOURCE_EXTRACTOR_DRONES: false
# APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false # APP_NO_RESOURCE_EXTRACTOR_DRONES_DAMAGE: false
# APP_SKIP_CLAN_KEY_CRAFTING: false # APP_SKIP_CLAN_KEY_CRAFTING: false

667
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -20,24 +20,28 @@
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.4.4", "json-with-bigint": "^3.2.2",
"mongoose": "^8.11.0", "mongoose": "^8.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"ncp": "^2.0.0", "ncp": "^2.0.0",
"typescript": "^5.5", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.67", "warframe-public-export-plus": "^0.5.59",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@rxliuli/tsgo": "^2025.3.31",
"@typescript-eslint/eslint-plugin": "^8.28.0", "@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0", "@typescript-eslint/parser": "^8.28.0",
"@typescript/native-preview": "^7.0.0-dev.20250523.1",
"eslint": "^8", "eslint": "^8",
"eslint-plugin-prettier": "^5.2.5", "eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0" "tsconfig-paths": "^4.2.0"
},
"engines": {
"node": ">=18.15.0",
"npm": ">=9.5.0"
} }
} }

View File

@ -21,13 +21,6 @@ app.use((req, _res, next) => {
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") { if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined; req.headers["content-encoding"] = undefined;
} }
// U18 uses application/x-www-form-urlencoded even tho the data is JSON which Express doesn't like.
// U17 sets no Content-Type at all, which Express also doesn't like.
if (!req.headers["content-type"] || req.headers["content-type"] == "application/x-www-form-urlencoded") {
req.headers["content-type"] = "application/octet-stream";
}
next(); next();
}); });

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel"; import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes"; import { IFriendInfo } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const addIgnoredUserController: RequestHandler = async (req, res) => { export const addIgnoredUserController: RequestHandler = async (req, res) => {

View File

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

View File

@ -1,7 +1,7 @@
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers"; import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
@ -75,7 +75,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!; const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType"); const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!; const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
const alliance = (await Alliance.findById(req.query.allianceId as string, "Name"))!; const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!;
await createMessage(invitedClanOwnerMember.accountId, [ await createMessage(invitedClanOwnerMember.accountId, [
{ {
sndr: getSuffixedName(account), sndr: getSuffixedName(account),
@ -95,7 +95,7 @@ export const addToAllianceController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title", sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: alliance._id.toString(), contextInfo: alliance._id.toString(),
highPriority: true, highPriority: true,
acceptAction: "ALLIANCE_INVITE", acceptAction: "ALLIANCE_INVITE",

View File

@ -1,10 +1,8 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { addInventoryDataToFriendInfo, areFriends } from "@/src/services/friendService"; import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
import { hasGuildPermission } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { getEffectiveAvatarImageType, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
@ -24,18 +22,15 @@ export const addToGuildController: RequestHandler = async (req, res) => {
return; return;
} }
const senderAccount = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "Settings"); const inventory = await getInventory(account._id.toString(), "Settings");
if ( // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE" || if (inventory.Settings?.GuildInvRestriction == "GIFT_MODE_NONE") {
(inventory.Settings?.GuildInvRestriction == "GIFT_MODE_FRIENDS" &&
!(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).json("Invite restricted"); res.status(400).json("Invite restricted");
return; return;
} }
const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!; const guild = (await Guild.findById(payload.GuildId.$oid, "Name Ranks"))!;
const senderAccount = await getAccountForRequest(req);
if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) { if (!(await hasGuildPermission(guild, senderAccount._id.toString(), GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission"); res.status(400).json("Invalid permission");
} }
@ -64,7 +59,7 @@ export const addToGuildController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title", sub: "/Lotus/Language/Menu/Mailbox_ClanInvite_Title",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: payload.GuildId.$oid, contextInfo: payload.GuildId.$oid,
highPriority: true, highPriority: true,
acceptAction: "GUILD_INVITE", acceptAction: "GUILD_INVITE",
@ -76,11 +71,10 @@ export const addToGuildController: RequestHandler = async (req, res) => {
const member: IGuildMemberClient = { const member: IGuildMemberClient = {
_id: { $oid: account._id.toString() }, _id: { $oid: account._id.toString() },
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
LastLogin: toMongoDate(account.LastLogin),
Rank: 7, Rank: 7,
Status: 2 Status: 2
}; };
await addInventoryDataToFriendInfo(member); await fillInInventoryDataForGuildMember(member);
res.json({ NewMember: member }); res.json({ NewMember: member });
} else if ("RequestMsg" in payload) { } else if ("RequestMsg" in payload) {
// Player applying to join a clan // Player applying to join a clan

View File

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

View File

@ -1,9 +1,9 @@
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper"; import { createVeiledRivenFingerprint, rivenRawToRealWeighted } from "@/src/helpers/rivenHelper";
import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, addMods, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService"; import { getRandomElement, getRandomWeightedReward, getRandomWeightedRewardUc } from "@/src/services/rngService";
import { IUpgradeFromClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { IOid } from "@/src/types/commonTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus"; import { ExportBoosterPacks, ExportUpgrades, TRarity } from "warframe-public-export-plus";
@ -24,7 +24,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
]); ]);
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
}); });
const rawRivenType = getRandomRawRivenType(); const rawRivenType = getRandomRawRivenType();
@ -57,8 +57,8 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType]; const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount; counts[meta.rarity] += upgrade.ItemCount;
if (fromOid(upgrade.ItemId) != "000000000000000000000000") { if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: fromOid(upgrade.ItemId) }); inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else { } else {
addMods(inventory, [ addMods(inventory, [
{ {
@ -128,14 +128,24 @@ const getRandomRawRivenType = (): string => {
}; };
interface IArtifactTransmutationRequest { interface IArtifactTransmutationRequest {
Upgrade: IUpgradeFromClient; Upgrade: IAgnosticUpgradeClient;
LevelDiff: number; LevelDiff: number;
Consumed: IUpgradeFromClient[]; Consumed: IAgnosticUpgradeClient[];
Cost: number; Cost: number;
FusionPointCost: number; FusionPointCost: number;
RivenTransmute?: boolean; RivenTransmute?: boolean;
} }
interface IAgnosticUpgradeClient {
ItemType: string;
ItemId: IOid;
FromSKU: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOid;
}
const specialModSets: string[][] = [ const specialModSets: string[][] = [
[ [
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",

View File

@ -4,9 +4,9 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
getInventory, getInventory,
updateCurrency, updateCurrency,
@ -17,11 +17,8 @@ import {
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot, IPendingRecipeDatabase, Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid2 } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
@ -29,8 +26,10 @@ interface IClaimCompletedRecipeRequest {
export const claimCompletedRecipeController: RequestHandler = async (req, res) => { export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body)); const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString()); if (!accountId) throw new Error("no account id");
const inventory = await getInventory(accountId);
const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid); const pendingRecipe = inventory.PendingRecipes.id(claimCompletedRecipeRequest.RecipeIds[0].$oid);
if (!pendingRecipe) { if (!pendingRecipe) {
throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`); throw new Error(`no pending recipe found with id ${claimCompletedRecipeRequest.RecipeIds[0].$oid}`);
@ -49,14 +48,40 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} }
if (req.query.cancel) { if (req.query.cancel) {
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe); ...updateCurrency(inventory, recipe.buildPrice * -1, false)
};
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
await inventory.save(); await inventory.save();
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root. res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOidWithLegacySupport[]; let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= []; inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= []; inventory.SpectreLoadouts ??= [];
@ -81,7 +106,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)), inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1 1
); );
BrandedSuits = [toOid2(pendingRecipe.SuitToUnbrand!, account.BuildLabel)]; BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
} }
let InventoryChanges: IInventoryChanges = {}; let InventoryChanges: IInventoryChanges = {};
@ -105,21 +130,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
...updateCurrency(inventory, cost, true) ...updateCurrency(inventory, cost, true)
}; };
} }
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
if (pet.Details!.HatchDate!.getTime() > Date.now()) {
pet.Details!.HatchDate = new Date();
}
let canSetActive = true;
for (const pet of inventory.KubrowPets) {
if (pet.Details!.Status == Status.StatusAvailable) {
canSetActive = false;
break;
}
}
pet.Details!.Status = canSetActive ? Status.StatusAvailable : Status.StatusStasis;
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await addItem( ...(await addItem(
@ -132,46 +143,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
)) ))
}; };
} }
if (
config.claimingBlueprintRefundsIngredients &&
recipe.secretIngredientAction != "SIA_CREATE_KUBROW" // Can't refund the egg
) {
await refundRecipeIngredients(inventory, InventoryChanges, recipe, pendingRecipe);
}
await inventory.save(); await inventory.save();
res.json({ InventoryChanges, BrandedSuits }); res.json({ InventoryChanges, BrandedSuits });
} }
}; };
const refundRecipeIngredients = async (
inventory: TInventoryDatabaseDocument,
inventoryChanges: IInventoryChanges,
recipe: IRecipe,
pendingRecipe: IPendingRecipeDatabase
): Promise<void> => {
updateCurrency(inventory, recipe.buildPrice * -1, false, inventoryChanges);
const equipmentIngredients = new Set();
for (const category of ["LongGuns", "Pistols", "Melee"] as const) {
if (pendingRecipe[category]) {
pendingRecipe[category].forEach(item => {
const index = inventory[category].push(item) - 1;
inventoryChanges[category] ??= [];
inventoryChanges[category].push(inventory[category][index].toJSON<IEquipmentClient>());
equipmentIngredients.add(item.ItemType);
occupySlot(inventory, InventorySlot.WEAPONS, false);
inventoryChanges.WeaponBin ??= { Slots: 0 };
inventoryChanges.WeaponBin.Slots -= 1;
});
}
}
for (const ingredient of recipe.ingredients) {
if (!equipmentIngredients.has(ingredient.ItemType)) {
combineInventoryChanges(
inventoryChanges,
await addItem(inventory, ingredient.ItemType, ingredient.ItemCount)
);
}
}
};

View File

@ -62,7 +62,7 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
await guild.save(); await guild.save();
res.json({ res.json({
...(await getGuildClient(guild, account)), ...(await getGuildClient(guild, account._id.toString())),
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges
}); });
} else { } else {

View File

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

View File

@ -1,5 +1,4 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
@ -8,23 +7,15 @@ import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => { export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers NemesisHistory"); const inventory = await getInventory(accountId, "CrewMembers");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body)); const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
if (data.crewMember.SecondInCommand) { const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
clearOnCall(inventory); dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
} dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
if (data.crewMember.ItemId.$oid == "000000000000000000000000") { dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
const convertedNemesis = inventory.NemesisHistory!.find(x => x.fp == data.crewMember.NemesisFingerprint)!; dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
convertedNemesis.SecondInCommand = data.crewMember.SecondInCommand; dbCrewMember.Configs = data.crewMember.Configs;
} else { dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
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(); await inventory.save();
res.json({ res.json({
crewMemberId: data.crewMember.ItemId.$oid, crewMemberId: data.crewMember.ItemId.$oid,
@ -35,20 +26,3 @@ export const crewMembersController: RequestHandler = async (req, res) => {
interface ICrewMembersRequest { interface ICrewMembersRequest {
crewMember: ICrewMemberClient; crewMember: ICrewMemberClient;
} }
const clearOnCall = (inventory: TInventoryDatabaseDocument): void => {
for (const cm of inventory.CrewMembers) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
if (inventory.NemesisHistory) {
for (const cm of inventory.NemesisHistory) {
if (cm.SecondInCommand) {
cm.SecondInCommand = false;
return;
}
}
}
};

View File

@ -1,529 +1,60 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IEndlessXpReward, IInventoryClient, TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEndlessXpCategory } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ExportRewards, ICountedStoreItem } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const endlessXpController: RequestHandler = async (req, res) => { export const endlessXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<IEndlessXpRequest>(String(req.body)); const payload = getJSONfromString<IEndlessXpRequest>(String(req.body));
if (payload.Mode == "r") {
const inventory = await getInventory(accountId, "EndlessXP");
inventory.EndlessXP ??= [];
let entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (!entry) {
entry = {
Category: payload.Category,
Earn: 0,
Claim: 0,
Choices: payload.Choices,
PendingRewards: []
};
inventory.EndlessXP.push(entry);
}
const weekStart = 1734307200_000 + Math.trunc((Date.now() - 1734307200_000) / 604800000) * 604800000; inventory.EndlessXP ??= [];
const weekEnd = weekStart + 604800000; const entry = inventory.EndlessXP.find(x => x.Category == payload.Category);
if (entry) {
entry.Earn = 0;
entry.Claim = 0;
entry.BonusAvailable = new Date(weekStart);
entry.Expiry = new Date(weekEnd);
entry.Choices = payload.Choices; entry.Choices = payload.Choices;
entry.PendingRewards =
payload.Category == "EXC_HARD"
? generateHardModeRewards(payload.Choices)
: generateNormalModeRewards(payload.Choices);
await inventory.save();
res.json({
NewProgress: inventory.toJSON<IInventoryClient>().EndlessXP!.find(x => x.Category == payload.Category)!
});
} else if (payload.Mode == "c") {
const inventory = await getInventory(accountId);
const entry = inventory.EndlessXP!.find(x => x.Category == payload.Category)!;
const inventoryChanges: IInventoryChanges = {};
for (const reward of entry.PendingRewards) {
if (entry.Claim < reward.RequiredTotalXp && reward.RequiredTotalXp <= entry.Earn) {
combineInventoryChanges(
inventoryChanges,
(
await handleStoreItemAcquisition(
reward.Rewards[0].StoreItem,
inventory,
reward.Rewards[0].ItemCount
)
).InventoryChanges
);
}
}
entry.Claim = entry.Earn;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
ClaimedXp: entry.Claim
});
} else { } else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); inventory.EndlessXP.push({
throw new Error(`unexpected endlessXp mode: ${payload.Mode}`); Category: payload.Category,
Choices: payload.Choices
});
} }
}; await inventory.save();
type IEndlessXpRequest = res.json({
| { NewProgress: {
Mode: "r"; Category: payload.Category,
Category: TEndlessXpCategory; Earn: 0,
Choices: string[]; Claim: 0,
} BonusAvailable: {
| { $date: {
Mode: "c" | "something else"; $numberLong: "9999999999999"
Category: TEndlessXpCategory;
};
const generateRandomRewards = (deckName: string): ICountedStoreItem[] => {
const reward = getRandomElement(ExportRewards[deckName][0])!;
return [
{
StoreItem: reward.type,
ItemCount: reward.itemCount
}
];
};
const normalModeChosenRewards: Record<string, string[]> = {
Excalibur: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Excalibur/RadialJavelinAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ExcaliburBlueprint"
],
Trinity: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trinity/EnergyVampireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinitySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrinityBlueprint"
],
Ember: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ember/WorldOnFireAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/EmberBlueprint"
],
Loki: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Loki/InvisibilityAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKISystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/LOKIBlueprint"
],
Mag: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Mag/CrushAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagBlueprint"
],
Rhino: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Rhino/RhinoChargeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RhinoBlueprint"
],
Ash: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ninja/GlaiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/AshBlueprint"
],
Frost: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Frost/IceShieldAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FrostBlueprint"
],
Nyx: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Jade/SelfBulletAttractorAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NyxBlueprint"
],
Saryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Saryn/PoisonAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/SarynBlueprint"
],
Vauban: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Trapper/LevTrapAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TrapperBlueprint"
],
Nova: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/AntiMatter/MolecularPrimeAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NovaBlueprint"
],
Nekros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Necro/CloneTheDeadAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NecroBlueprint"
],
Valkyr: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Berserker/IntimidateAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BerserkerBlueprint"
],
Oberon: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Paladin/RegenerationAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PaladinBlueprint"
],
Hydroid: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pirate/CannonBarrageAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HydroidBlueprint"
],
Mirage: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Harlequin/LightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/HarlequinBlueprint"
],
Limbo: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Magician/TearInSpaceAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MagicianBlueprint"
],
Mesa: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Cowgirl/GunFuPvPAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GunslingerBlueprint"
],
Chroma: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Dragon/DragonLuckAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/ChromaBlueprint"
],
Atlas: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Brawler/BrawlerPassiveAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/BrawlerBlueprint"
],
Ivara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Ranger/RangerStealAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RangerBlueprint"
],
Inaros: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Sandman/SandmanSwarmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/MummyBlueprint"
],
Titania: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Fairy/FairyFlightAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairySystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/FairyBlueprint"
],
Nidus: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Infestation/InfestPodsAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/NidusBlueprint"
],
Octavia: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Bard/BardCharmAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/OctaviaBlueprint"
],
Harrow: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Priest/PriestPactAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PriestBlueprint"
],
Gara: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Glass/GlassFragmentAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GlassBlueprint"
],
Khora: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Khora/KhoraCrackAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/KhoraBlueprint"
],
Revenant: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Revenant/RevenantMarkAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/RevenantBlueprint"
],
Garuda: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Garuda/GarudaUnstoppableAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/GarudaBlueprint"
],
Baruuk: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistChassisBlueprint",
"/Lotus/StoreItems/Powersuits/Pacifist/PacifistFistAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/PacifistBlueprint"
],
Hildryn: [
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeHelmetBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeChassisBlueprint",
"/Lotus/StoreItems/Powersuits/IronFrame/IronFrameStripAugmentCard",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeSystemsBlueprint",
"/Lotus/StoreItems/Types/Recipes/WarframeRecipes/IronframeBlueprint"
]
};
const generateNormalModeRewards = (choices: string[]): IEndlessXpReward[] => {
const choiceRewards = normalModeChosenRewards[choices[0]];
return [
{
RequiredTotalXp: 190,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 400,
Rewards: [
{
StoreItem: choiceRewards[0],
ItemCount: 1
} }
] },
}, Expiry: {
{ $date: {
RequiredTotalXp: 630, $numberLong: "9999999999999"
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalSilverRewards"
)
},
{
RequiredTotalXp: 890,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalMODRewards"
)
},
{
RequiredTotalXp: 1190,
Rewards: [
{
StoreItem: choiceRewards[1],
ItemCount: 1
} }
] },
}, Choices: payload.Choices,
{ PendingRewards: [
RequiredTotalXp: 1540,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalGoldRewards"
)
},
{
RequiredTotalXp: 1950,
Rewards: [
{ {
StoreItem: choiceRewards[2], RequiredTotalXp: 190,
ItemCount: 1 Rewards: [
} {
] StoreItem: "/Lotus/StoreItems/Upgrades/Mods/Aura/PlayerHealthAuraMod",
}, ItemCount: 1
{ }
RequiredTotalXp: 2430, ]
Rewards: [
{
StoreItem: choiceRewards[3],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2990,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessNormalArcaneRewards"
)
},
{
RequiredTotalXp: 3640,
Rewards: [
{
StoreItem: choiceRewards[4],
ItemCount: 1
} }
// ...
] ]
} }
]; });
}; };
const hardModeChosenRewards: Record<string, string> = { interface IEndlessXpRequest {
Braton: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BratonIncarnonUnlocker", Mode: string; // "r"
Lato: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LatoIncarnonUnlocker", Category: TEndlessXpCategory;
Skana: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SkanaIncarnonUnlocker", Choices: string[];
Paris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ParisIncarnonUnlocker", }
Kunai: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/KunaiIncarnonUnlocker",
Boar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoarIncarnonUnlocker",
Gammacor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/GammacorIncarnonUnlocker",
Anku: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AnkuIncarnonUnlocker",
Gorgon: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/GorgonIncarnonUnlocker",
Angstrum: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AngstrumIncarnonUnlocker",
Bo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/BoIncarnonUnlocker",
Latron: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/LatronIncarnonUnlocker",
Furis: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/FurisIncarnonUnlocker",
Furax: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/FuraxIncarnonUnlocker",
Strun: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/StrunIncarnonUnlocker",
Lex: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/LexIncarnonUnlocker",
Magistar: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/MagistarIncarnonUnlocker",
Boltor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BoltorIncarnonUnlocker",
Bronco: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/BroncoIncarnonUnlocker",
CeramicDagger: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/CeramicDaggerIncarnonUnlocker",
Torid: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/ToridIncarnonUnlocker",
DualToxocyst: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DualToxocystIncarnonUnlocker",
DualIchor: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/DualIchorIncarnonUnlocker",
Miter: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/MiterIncarnonUnlocker",
Atomos: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/AtomosIncarnonUnlocker",
AckAndBrunt: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/AckAndBruntIncarnonUnlocker",
Soma: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SomaIncarnonUnlocker",
Vasto: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/VastoIncarnonUnlocker",
NamiSolo: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/NamiSoloIncarnonUnlocker",
Burston: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/BurstonIncarnonUnlocker",
Zylok: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/ZylokIncarnonUnlocker",
Sibear: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/SibearIncarnonUnlocker",
Dread: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DreadIncarnonUnlocker",
Despair: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/DespairIncarnonUnlocker",
Hate: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/HateIncarnonUnlocker",
Dera: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/DeraIncarnonUnlocker",
Cestra: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/CestraIncarnonUnlocker",
Okina: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Melee/OkinaIncarnonUnlocker",
Sybaris: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Primary/SybarisIncarnonUnlocker",
Sicarus: "/Lotus/StoreItems/Types/Items/MiscItems/IncarnonAdapters/Secondary/SicarusIncarnonUnlocker",
RivenPrimary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
RivenSecondary: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawPistolRandomMod",
RivenMelee: "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawMeleeRandomMod",
Kuva: "/Lotus/Types/Game/DuviriEndless/CircuitSteelPathBIGKuvaReward"
};
const generateHardModeRewards = (choices: string[]): IEndlessXpReward[] => {
return [
{
RequiredTotalXp: 285,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 600,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 945,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1335,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSilverRewards"
)
},
{
RequiredTotalXp: 1785,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[0]],
ItemCount: 1
}
]
},
{
RequiredTotalXp: 2310,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 2925,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathGoldRewards"
)
},
{
RequiredTotalXp: 3645,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathArcaneRewards"
)
},
{
RequiredTotalXp: 4485,
Rewards: generateRandomRewards(
"/Lotus/Types/Game/MissionDecks/DuviriEndlessCircuitRewards/DuviriEndlessSteelPathSteelEssenceRewards"
)
},
{
RequiredTotalXp: 5460,
Rewards: [
{
StoreItem: hardModeChosenRewards[choices[1]],
ItemCount: 1
}
]
}
];
};

View File

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

View File

@ -64,9 +64,7 @@ export const focusController: RequestHandler = async (req, res) => {
} }
); );
res.json({ res.end();
FocusUpgrade: { ItemType: focusType }
});
break; break;
} }
case FocusOperation.UnlockUpgrade: { case FocusOperation.UnlockUpgrade: {

View File

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

View File

@ -1,13 +1,13 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Guild } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
export const getGuildController: RequestHandler = async (req, res) => { export const getGuildController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId"); const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) { if (inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId); const guild = await Guild.findById(inventory.GuildId);
if (guild) { if (guild) {
@ -24,7 +24,7 @@ export const getGuildController: RequestHandler = async (req, res) => {
guild.CeremonyResetDate = undefined; guild.CeremonyResetDate = undefined;
await guild.save(); await guild.save();
} }
res.json(await getGuildClient(guild, account)); res.json(await getGuildClient(guild, accountId));
return; return;
} }
} }

View File

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

View File

@ -1,7 +1,7 @@
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { Account, Ignore } from "@/src/models/loginModel"; import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/friendTypes"; import { IFriendInfo } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils"; import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express"; import { RequestHandler } from "express";

View File

@ -3,6 +3,7 @@ import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json"; import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService"; import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes"; import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes"; import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
@ -20,6 +21,7 @@ export const getShipController: RequestHandler = async (req, res) => {
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>(); const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
const loadout = await getLoadout(accountId); const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
const getShipResponse: IGetShipResponse = { const getShipResponse: IGetShipResponse = {
ShipOwnerId: accountId, ShipOwnerId: accountId,
@ -29,8 +31,8 @@ export const getShipController: RequestHandler = async (req, res) => {
ShipId: toOid(personalRoomsDb.activeShipId), ShipId: toOid(personalRoomsDb.activeShipId),
ShipInterior: { ShipInterior: {
Colors: personalRooms.ShipInteriorColors, Colors: personalRooms.ShipInteriorColors,
ShipAttachments: { HOOD_ORNAMENT: "" }, ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: "" SkinFlavourItem: ship.SkinFlavourItem
}, },
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId) ? toOid(personalRooms.Ship.FavouriteLoadoutId)

View File

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

View File

@ -1,19 +1,12 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { areFriends } from "@/src/services/friendService";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { import { getInventory, updateCurrency } from "@/src/services/inventoryService";
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { IInventoryChanges, IPurchaseParams } from "@/src/types/purchaseTypes"; import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportBundles, ExportFlavour } from "warframe-public-export-plus"; import { ExportFlavour } from "warframe-public-export-plus";
export const giftingController: RequestHandler = async (req, res) => { export const giftingController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGiftingRequest>(String(req.body)); const data = getJSONfromString<IGiftingRequest>(String(req.body));
@ -37,11 +30,8 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
// Cannot gift to players who have gifting disabled. // Cannot gift to players who have gifting disabled.
const senderAccount = await getAccountForRequest(req); // TODO: Also consider GIFT_MODE_FRIENDS once friends are implemented
if ( if (inventory.Settings?.GiftMode == "GIFT_MODE_NONE") {
inventory.Settings?.GiftMode == "GIFT_MODE_NONE" ||
(inventory.Settings?.GiftMode == "GIFT_MODE_FRIENDS" && !(await areFriends(account._id, senderAccount._id)))
) {
res.status(400).send("17").end(); res.status(400).send("17").end();
return; return;
} }
@ -50,7 +40,11 @@ export const giftingController: RequestHandler = async (req, res) => {
// TODO: Cannot gift archwing items to players that have not completed the archwing quest. (Code 7) // TODO: Cannot gift 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) // TODO: Cannot gift necramechs to players that have not completed heart of deimos. (Code 20)
const senderInventory = await getInventory(senderAccount._id.toString()); const senderAccount = await getAccountForRequest(req);
const senderInventory = await getInventory(
senderAccount._id.toString(),
"PremiumCredits PremiumCreditsFree ActiveAvatarImageType GiftsRemaining"
);
if (senderInventory.GiftsRemaining == 0) { if (senderInventory.GiftsRemaining == 0) {
res.status(400).send("10").end(); res.status(400).send("10").end();
@ -58,20 +52,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
senderInventory.GiftsRemaining -= 1; senderInventory.GiftsRemaining -= 1;
const inventoryChanges: IInventoryChanges = updateCurrency( updateCurrency(senderInventory, data.PurchaseParams.ExpectedPrice, true);
senderInventory,
data.PurchaseParams.ExpectedPrice,
true
);
if (data.PurchaseParams.StoreItem in ExportBundles) {
const bundle = ExportBundles[data.PurchaseParams.StoreItem];
if (bundle.giftingBonus) {
combineInventoryChanges(
inventoryChanges,
(await handleStoreItemAcquisition(bundle.giftingBonus, senderInventory)).InventoryChanges
);
}
}
await senderInventory.save(); await senderInventory.save();
const senderName = getSuffixedName(senderAccount); const senderName = getSuffixedName(senderAccount);
@ -90,7 +71,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/GiftReceivedSubject", sub: "/Lotus/Language/Menu/GiftReceivedSubject",
icon: ExportFlavour[getEffectiveAvatarImageType(senderInventory)].icon, icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
gifts: [ gifts: [
{ {
GiftType: data.PurchaseParams.StoreItem GiftType: data.PurchaseParams.StoreItem
@ -99,9 +80,7 @@ export const giftingController: RequestHandler = async (req, res) => {
} }
]); ]);
res.json({ res.end();
InventoryChanges: inventoryChanges
});
}; };
interface IGiftingRequest { interface IGiftingRequest {

View File

@ -104,7 +104,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
) { ) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`); throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
} }
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId!)) { if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
throw new Error( throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array` `no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
); );

View File

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

View File

@ -9,12 +9,7 @@ import {
getMessage getMessage
} from "@/src/services/inboxService"; } from "@/src/services/inboxService";
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
addItems,
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus"; import { ExportFlavour } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
@ -93,7 +88,7 @@ export const inboxController: RequestHandler = async (req, res) => {
} }
], ],
sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject", sub: "/Lotus/Language/Menu/GiftReceivedConfirmationSubject",
icon: ExportFlavour[getEffectiveAvatarImageType(inventory)].icon, icon: ExportFlavour[inventory.ActiveAvatarImageType].icon,
highPriority: true highPriority: true
} }
]); ]);

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService"; import { getInventory, addMiscItems, updateCurrency, addRecipes, freeUpSlot } from "@/src/services/inventoryService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
@ -12,7 +12,7 @@ import {
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc } from "warframe-public-export-plus"; import { ExportMisc } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper"; import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
@ -23,12 +23,12 @@ import {
} from "@/src/services/infestedFoundryService"; } from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
switch (req.query.mode) { switch (req.query.mode) {
case "s": { case "s": {
// shard installation // shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
@ -56,7 +56,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "x": { case "x": {
// shard removal // shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body)); const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
@ -70,30 +70,19 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
ItemCount: 1 ItemCount: 1
}); });
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
// consume resources
if (!config.infiniteHelminthMaterials) {
let type: string;
let count: number;
if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
// < 38.6.0
type = "/Lotus/Types/Items/InfestedFoundry/HelminthBile";
count = 300;
} else {
// >= 38.6.0
type =
archonCrystalRemovalResource[
suit.ArchonCrystalUpgrades![request.Slot].Color!.replace("_MYTHIC", "")
];
count = suit.ArchonCrystalUpgrades![request.Slot].Color!.indexOf("_MYTHIC") != -1 ? 300 : 150;
}
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == type)!.Count -= count;
}
} }
// remove from suit // remove from suit
suit.ArchonCrystalUpgrades![request.Slot] = {}; suit.ArchonCrystalUpgrades![request.Slot] = {};
if (!config.infiniteHelminthMaterials) {
// remove bile
const bile = inventory.InfestedFoundry!.Resources!.find(
x => x.ItemType == "/Lotus/Types/Items/InfestedFoundry/HelminthBile"
)!;
bile.Count -= 300;
}
await inventory.save(); await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!; const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
@ -110,7 +99,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "n": { case "n": {
// name the beast // name the beast
const request = getJSONfromString<IHelminthNameRequest>(String(req.body)); const request = getJSONfromString<IHelminthNameRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Name = request.newName; inventory.InfestedFoundry.Name = request.newName;
await inventory.save(); await inventory.save();
@ -133,7 +122,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body)); const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Resources ??= []; inventory.InfestedFoundry.Resources ??= [];
@ -229,7 +218,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "o": { case "o": {
// offerings update // offerings update
const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body)); const request = getJSONfromString<IHelminthOfferingsUpdate>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex; inventory.InfestedFoundry.InvigorationIndex = request.OfferingsIndex;
inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes; inventory.InfestedFoundry.InvigorationSuitOfferings = request.SuitTypes;
@ -250,7 +239,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "a": { case "a": {
// subsume warframe // subsume warframe
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body)); const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const recipe = getRecipe(request.Recipe)!; const recipe = getRecipe(request.Recipe)!;
if (!config.infiniteHelminthMaterials) { if (!config.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) { for (const ingredient of recipe.secretIngredients!) {
@ -300,7 +289,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "r": { case "r": {
// rush subsume // rush subsume
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 50, true); const currencyChanges = updateCurrency(inventory, 50, true);
const recipeChanges = handleSubsumeCompletion(inventory); const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save(); await inventory.save();
@ -318,7 +307,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "u": { case "u": {
const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body)); const request = getJSONfromString<IHelminthInvigorationRequest>(String(req.body));
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); const upgradesExpiry = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
suit.OffensiveUpgrade = request.OffensiveUpgradeType; suit.OffensiveUpgrade = request.OffensiveUpgradeType;
@ -351,7 +340,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
} }
case "custom_unlockall": { case "custom_unlockall": {
const inventory = await getInventory(account._id.toString()); const inventory = await getInventory(accountId);
inventory.InfestedFoundry ??= {}; inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.XP ??= 0; inventory.InfestedFoundry.XP ??= 0;
if (151875_00 > inventory.InfestedFoundry.XP) { if (151875_00 > inventory.InfestedFoundry.XP) {
@ -450,12 +439,3 @@ const apetiteModel = (x: number): number => {
} }
return 3; return 3;
}; };
const archonCrystalRemovalResource: Record<string, string> = {
ACC_RED: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides",
ACC_YELLOW: "/Lotus/Types/Items/InfestedFoundry/HelminthBile",
ACC_BLUE: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics",
ACC_GREEN: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics",
ACC_ORANGE: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones",
ACC_PURPLE: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx"
};

View File

@ -24,11 +24,7 @@ import {
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers"; import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { getNemesisManifest } from "@/src/helpers/nemesisHelpers"; import { isNemesisCompatibleWithVersion } from "@/src/helpers/nemesisHelpers";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { Ship } from "@/src/models/shipModel";
import { toLegacyOid, version_compare } from "@/src/helpers/inventoryHelpers";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const account = await getAccountForRequest(request);
@ -134,12 +130,13 @@ export const getInventoryResponse = async (
xpBasedLevelCapDisabled: boolean, xpBasedLevelCapDisabled: boolean,
buildLabel: string | undefined buildLabel: string | undefined
): Promise<IInventoryClient> => { ): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships] = await Promise.all([ const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>(
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"), "LoadOutPresets"
Ship.find({ ShipOwnerId: inventory.accountOwnerId }) );
]); const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>(
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>(); "Ships"
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>()); );
const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON<IInventoryClient>();
if (config.infiniteCredits) { if (config.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999; inventoryResponse.RegularCredits = 999999999;
@ -306,44 +303,20 @@ export const getInventoryResponse = async (
// Set 2FA enabled so trading post can be used // Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true; inventoryResponse.HWIDProtectEnabled = true;
if (buildLabel) { // Fix nemesis for older versions
// Fix nemesis for older versions if (
if ( inventoryResponse.Nemesis &&
inventoryResponse.Nemesis && buildLabel &&
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0 !isNemesisCompatibleWithVersion(inventoryResponse.Nemesis, buildLabel)
) { ) {
inventoryResponse.Nemesis = undefined; inventoryResponse.Nemesis = undefined;
}
if (version_compare(buildLabel, "2018.02.22.14.34") < 0) {
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
if (version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
// U19.5 and below use $id instead of $oid
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
}
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
}
}
} }
return inventoryResponse; return inventoryResponse;
}; };
const addString = (arr: string[], str: string): void => { const addString = (arr: string[], str: string): void => {
if (arr.indexOf(str) == -1) { if (!arr.find(x => x == str)) {
arr.push(str); arr.push(str);
} }
}; };

View File

@ -7,7 +7,7 @@ import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService"; import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes"; import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/helpers/inventoryHelpers"; import { version_compare } from "@/src/services/worldStateService";
export const loginController: RequestHandler = async (request, response) => { export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
@ -20,21 +20,7 @@ export const loginController: RequestHandler = async (request, response) => {
? request.query.buildLabel.split(" ").join("+") ? request.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel; : buildConfig.buildLabel;
let myAddress: string; const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
let myUrlBase: string = request.protocol + "://";
if (request.host.indexOf("warframe.com") == -1) {
// Client request was redirected cleanly, so we know it can reach us how it's reaching us now.
myAddress = request.hostname;
myUrlBase += request.host;
} else {
// Don't know how the client reached us, hoping the config does.
myAddress = config.myAddress;
myUrlBase += myAddress;
const port: number = request.protocol == "http" ? config.httpPort || 80 : config.httpsPort || 443;
if (port != (request.protocol == "http" ? 80 : 443)) {
myUrlBase += ":" + port;
}
}
if ( if (
!account && !account &&
@ -55,18 +41,17 @@ export const loginController: RequestHandler = async (request, response) => {
email: loginRequest.email, email: loginRequest.email,
password: loginRequest.password, password: loginRequest.password,
DisplayName: name, DisplayName: name,
CountryCode: loginRequest.lang?.toUpperCase() ?? "EN", CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType, ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
CrossPlatformAllowed: true, CrossPlatformAllowed: true,
ForceLogoutVersion: 0, ForceLogoutVersion: 0,
ConsentNeeded: false, ConsentNeeded: false,
TrackedSettings: [], TrackedSettings: [],
Nonce: nonce, Nonce: nonce,
BuildLabel: buildLabel, BuildLabel: buildLabel
LastLogin: new Date()
}); });
logger.debug("created new account"); logger.debug("created new account");
response.json(createLoginResponse(myAddress, myUrlBase, newAccount, buildLabel)); response.json(createLoginResponse(myAddress, newAccount, buildLabel));
return; return;
} catch (error: unknown) { } catch (error: unknown) {
if (error instanceof Error) { if (error instanceof Error) {
@ -97,30 +82,21 @@ export const loginController: RequestHandler = async (request, response) => {
} }
} else { } else {
if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) { if (account.Nonce && account.ClientType != "webui" && !account.Dropped && !loginRequest.kick) {
// U17 seems to handle "nonce still set" like a login failure. response.status(400).json({ error: "nonce still set" });
if (version_compare(buildLabel, "2015.12.05.18.07") >= 0) { return;
response.status(400).send({ error: "nonce still set" });
return;
}
} }
account.ClientType = loginRequest.ClientType; account.ClientType = loginRequest.ClientType;
account.Nonce = nonce; account.Nonce = nonce;
account.CountryCode = loginRequest.lang?.toUpperCase() ?? "EN"; account.CountryCode = loginRequest.lang.toUpperCase();
account.BuildLabel = buildLabel; account.BuildLabel = buildLabel;
account.LastLogin = new Date();
} }
await account.save(); await account.save();
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); response.json(createLoginResponse(myAddress, account.toJSON(), buildLabel));
}; };
const createLoginResponse = ( const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
myAddress: string,
myUrlBase: string,
account: IDatabaseAccountJson,
buildLabel: string
): ILoginResponse => {
const resp: ILoginResponse = { const resp: ILoginResponse = {
id: account.id, id: account.id,
DisplayName: account.DisplayName, DisplayName: account.DisplayName,
@ -128,15 +104,10 @@ const createLoginResponse = (
AmazonAuthToken: account.AmazonAuthToken, AmazonAuthToken: account.AmazonAuthToken,
AmazonRefreshToken: account.AmazonRefreshToken, AmazonRefreshToken: account.AmazonRefreshToken,
Nonce: account.Nonce, Nonce: account.Nonce,
IRC: config.myIrcAddresses ?? [myAddress],
NRS: config.NRS,
BuildLabel: buildLabel BuildLabel: buildLabel
}; };
if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) {
resp.NRS = config.NRS;
}
if (version_compare(buildLabel, "2015.05.14.16.29") >= 0) {
// U17 and up
resp.IRC = config.myIrcAddresses ?? [myAddress];
}
if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) { if (version_compare(buildLabel, "2018.11.08.14.45") >= 0) {
// U24 and up // U24 and up
resp.ConsentNeeded = account.ConsentNeeded; resp.ConsentNeeded = account.ConsentNeeded;
@ -158,11 +129,11 @@ const createLoginResponse = (
} }
if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) { if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
resp.CrossPlatformAllowed = account.CrossPlatformAllowed; resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
resp.HUB = `${myUrlBase}/api/`; resp.HUB = `https://${myAddress}/api/`;
resp.MatchmakingBuildId = buildConfig.matchmakingBuildId; resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
} }
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) { if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
resp.platformCDNs = [`${myUrlBase}/`]; resp.platformCDNs = [`https://${myAddress}/`];
} }
return resp; return resp;
}; };

View File

@ -26,7 +26,7 @@ export const loginRewardsSelectionController: RequestHandler = async (req, res)
StoreItemType: body.ChosenReward StoreItemType: body.ChosenReward
}; };
inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges; inventoryChanges = (await handleStoreItemAcquisition(body.ChosenReward, inventory)).InventoryChanges;
if (evergreenRewards.indexOf(body.ChosenReward) == -1) { if (!evergreenRewards.find(x => x == body.ChosenReward)) {
inventory.LoginMilestoneRewards.push(body.ChosenReward); inventory.LoginMilestoneRewards.push(body.ChosenReward);
} }
} else { } else {

View File

@ -57,15 +57,11 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
const firstCompletion = missionReport.SortieId const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1 ? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false; : false;
const inventoryUpdates = await addMissionInventoryUpdates(account, inventory, missionReport); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if ( if (
missionReport.MissionStatus !== "GS_SUCCESS" && missionReport.MissionStatus !== "GS_SUCCESS" &&
!( !(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
missionReport.RewardInfo?.jobId ||
missionReport.RewardInfo?.challengeMissionId ||
missionReport.RewardInfo?.T
)
) { ) {
if (missionReport.EndOfMatchUpload) { if (missionReport.EndOfMatchUpload) {
inventory.RewardSeed = generateRewardSeed(); inventory.RewardSeed = generateRewardSeed();

View File

@ -2,7 +2,7 @@ import { RequestHandler } from "express";
import { ExportWeapons } from "warframe-public-export-plus"; import { ExportWeapons } from "warframe-public-export-plus";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { SRng } from "@/src/services/rngService"; import { CRng } from "@/src/services/rngService";
import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { import {
@ -140,7 +140,7 @@ const getModularWeaponSale = (
partTypes: string[], partTypes: string[],
getItemType: (parts: string[]) => string getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => { ): IModularWeaponSaleInfo => {
const rng = new SRng(day); 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; let partsCost = 0;
for (const part of parts) { for (const part of parts) {

View File

@ -1,24 +1,18 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { import {
consumeModCharge, consumeModCharge,
decodeNemesisGuess,
encodeNemesisGuess, encodeNemesisGuess,
getInfNodes, getInfNodes,
getKnifeUpgrade, getKnifeUpgrade,
getNemesisManifest,
getNemesisPasscode, getNemesisPasscode,
getNemesisPasscodeModTypes, getNemesisPasscodeModTypes,
GUESS_CORRECT, getWeaponsForManifest,
GUESS_INCORRECT, IKnifeResponse,
GUESS_NEUTRAL, showdownNodes
GUESS_NONE,
GUESS_WILDCARD,
IKnifeResponse
} from "@/src/helpers/nemesisHelpers"; } from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -30,17 +24,16 @@ import {
IUpgradeClient, IUpgradeClient,
IWeaponSkinClient, IWeaponSkinClient,
LoadoutIndex, LoadoutIndex,
TEquipmentKey, TEquipmentKey
TNemesisFaction
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const nemesisController: RequestHandler = async (req, res) => { export const nemesisController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
if ((req.query.mode as string) == "f") { if ((req.query.mode as string) == "f") {
const body = getJSONfromString<IValenceFusionRequest>(String(req.body)); const body = getJSONfromString<IValenceFusionRequest>(String(req.body));
const inventory = await getInventory(account._id.toString(), body.Category + " WeaponBin"); const inventory = await getInventory(accountId, body.Category + " WeaponBin");
const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!; const destWeapon = inventory[body.Category].id(body.DestWeapon.$oid)!;
const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!; const sourceWeapon = inventory[body.Category].id(body.SourceWeapon.$oid)!;
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
@ -75,7 +68,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
}); });
} else if ((req.query.mode as string) == "p") { } else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body)); const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!); const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0; let guessResult = 0;
@ -88,7 +81,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
} else { } else {
for (let i = 0; i != 3; ++i) { for (let i = 0; i != 3; ++i) {
if (body.guess[i] == passcode[i] || body.guess[i] == GUESS_WILDCARD) { if (body.guess[i] == passcode[i]) {
++guessResult; ++guessResult;
} }
} }
@ -96,36 +89,25 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({ GuessResult: guessResult }); res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") { } else if (req.query.mode == "r") {
const inventory = await getInventory( const inventory = await getInventory(
account._id.toString(), accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
); );
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body)); const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") { if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf]; const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0]; const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
const result1 = passcode == guess[0] ? GUESS_CORRECT : GUESS_INCORRECT;
const result2 = passcode == guess[1] ? GUESS_CORRECT : GUESS_INCORRECT; // Add to GuessHistory
const result3 = passcode == guess[2] ? GUESS_CORRECT : GUESS_INCORRECT; 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( inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess([ encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
{
symbol: guess[0],
result: result1
},
{
symbol: guess[1],
result: result2
},
{
symbol: guess[2],
result: result3
}
])
); );
// Increase antivirus if correct antivirus mod is installed // Increase antivirus if correct antivirus mod is installed
const response: IKnifeResponse = {}; const response: IKnifeResponse = {};
if (result1 == GUESS_CORRECT || result2 == GUESS_CORRECT || result3 == GUESS_CORRECT) { if (result1 == 0 || result2 == 0 || result3 == 0) {
let antivirusGain = 5; let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!; const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid); const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
@ -161,89 +143,44 @@ export const nemesisController: RequestHandler = async (req, res) => {
if (inventory.Nemesis!.HenchmenKilled >= 100) { if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100; inventory.Nemesis!.HenchmenKilled = 100;
} }
inventory.Nemesis!.InfNodes = getInfNodes(getNemesisManifest(inventory.Nemesis!.manifest), 0); inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
await inventory.save(); await inventory.save();
res.json(response); res.json(response);
} else { } else {
// For first guess, create a new entry. const passcode = getNemesisPasscode(inventory.Nemesis!);
if (body.position == 0) { if (passcode[body.position] != body.guess) {
inventory.Nemesis!.GuessHistory.push( res.end();
encodeNemesisGuess([ } else {
{ inventory.Nemesis!.Rank += 1;
symbol: GUESS_NONE, inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
result: GUESS_NEUTRAL await inventory.save();
}, res.json({ RankIncrease: 1 });
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
},
{
symbol: GUESS_NONE,
result: GUESS_NEUTRAL
}
])
);
} }
// Evaluate guess
const correct =
body.guess == GUESS_WILDCARD || getNemesisPasscode(inventory.Nemesis!)[body.position] == body.guess;
// Update entry
const guess = decodeNemesisGuess(
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1]
);
guess[body.position].symbol = body.guess;
guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
// Increase rank if incorrect
let RankIncrease: number | undefined;
if (!correct) {
RankIncrease = 1;
const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
}
await inventory.save();
res.json({ RankIncrease });
} }
} else if ((req.query.mode as string) == "rs") { } else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body // report spawn; POST but no application data in body
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount; inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save(); await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc }); res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") { } else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(account._id.toString(), "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
if (inventory.Nemesis) {
logger.warn(`overwriting an existing nemesis as a new one is being requested`);
}
const body = getJSONfromString<INemesisStartRequest>(String(req.body)); const body = getJSONfromString<INemesisStartRequest>(String(req.body));
body.target.fp = BigInt(body.target.fp); body.target.fp = BigInt(body.target.fp);
const manifest = getNemesisManifest(body.target.manifest);
if (account.BuildLabel && version_compare(account.BuildLabel, manifest.minBuild) < 0) {
logger.warn(
`client on version ${account.BuildLabel} provided nemesis manifest ${body.target.manifest} which was expected to require ${manifest.minBuild} or above. please file a bug report.`
);
}
let weaponIdx = -1; let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") { if (body.target.Faction != "FC_INFESTATION") {
const weapons: readonly string[] = manifest.weapons; const weapons = getWeaponsForManifest(body.target.manifest);
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1); const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx; weaponIdx = initialWeaponIdx;
if (body.target.DisallowedWeapons) { do {
do { const weapon = weapons[weaponIdx];
const weapon = weapons[weaponIdx]; if (!body.target.DisallowedWeapons.find(x => x == weapon)) {
if (body.target.DisallowedWeapons.indexOf(weapon) == -1) { break;
break; }
} weaponIdx = (weaponIdx + 1) % weapons.length;
weaponIdx = (weaponIdx + 1) % weapons.length; } while (weaponIdx != initialWeaponIdx);
} while (weaponIdx != initialWeaponIdx);
}
} }
inventory.Nemesis = { inventory.Nemesis = {
@ -260,14 +197,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
k: false, k: false,
Traded: false, Traded: false,
d: new Date(), d: new Date(),
InfNodes: getInfNodes(manifest, 0), InfNodes: getInfNodes(body.target.Faction, 0),
GuessHistory: [], GuessHistory: [],
Hints: [], Hints: [],
HintProgress: 0, HintProgress: 0,
Weakened: false, Weakened: body.target.Weakened,
PrevOwners: 0, PrevOwners: 0,
HenchmenKilled: 0, HenchmenKilled: 0,
SecondInCommand: false, SecondInCommand: body.target.SecondInCommand,
MissionCount: 0, MissionCount: 0,
LastEnc: 0 LastEnc: 0
}; };
@ -278,14 +215,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
}); });
} else if ((req.query.mode as string) == "w") { } else if ((req.query.mode as string) == "w") {
const inventory = await getInventory( const inventory = await getInventory(
account._id.toString(), accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades" "Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
); );
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body)); //const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [ inventory.Nemesis!.InfNodes = [
{ {
Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode, Node: showdownNodes[inventory.Nemesis!.Faction],
Influence: 1 Influence: 1
} }
]; ];
@ -328,11 +265,11 @@ interface INemesisStartRequest {
KillingSuit: string; KillingSuit: string;
killingDamageType: number; killingDamageType: number;
ShoulderHelmet: string; ShoulderHelmet: string;
DisallowedWeapons?: string[]; DisallowedWeapons: string[];
WeaponIdx: number; WeaponIdx: number;
AgentIdx: number; AgentIdx: number;
BirthNode: string; BirthNode: string;
Faction: TNemesisFaction; Faction: string;
Rank: number; Rank: number;
k: boolean; k: boolean;
Traded: boolean; Traded: boolean;

View File

@ -2,16 +2,13 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { ExportRelics, IRelic } from "warframe-public-export-plus"; import { ExportRelics, IRelic } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const projectionManagerController: RequestHandler = async (req, res) => { export const projectionManagerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest; const request = JSON.parse(String(req.body)) as IProjectionUpgradeRequest;
const [era, category, currentQuality] = parseProjection(request.projectionType); const [era, category, currentQuality] = parseProjection(request.projectionType);
const upgradeCost = config.dontSubtractVoidTraces const upgradeCost = (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
? 0
: (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]); const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]);
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {

View File

@ -1,25 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
// Basic shim handling action=sync to login on U21
export const questControlController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const quests: IQuestState[] = [];
for (const quest of inventory.QuestKeys) {
quests.push({
quest: quest.ItemType,
state: 3 // COMPLETE
});
}
res.json({
QuestState: quests
});
};
interface IQuestState {
quest: string;
state: number;
task?: string;
}

View File

@ -1,99 +0,0 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { Account } from "@/src/models/loginModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const removeFriendGetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
if (req.query.all) {
const [internalFriendships, externalFriendships] = await Promise.all([
Friendship.find({ owner: accountId }, "friend"),
Friendship.find({ friend: accountId }, "owner")
]);
const promises: Promise<void>[] = [];
const friends: IOid[] = [];
for (const externalFriendship of externalFriendships) {
if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) {
promises.push(Friendship.deleteOne({ _id: externalFriendship._id }) as unknown as Promise<void>);
friends.push(toOid(externalFriendship.owner));
}
}
await Promise.all(promises);
res.json({
Friends: friends
} satisfies IRemoveFriendsResponse);
} else {
const friendId = req.query.friendId as string;
await Promise.all([
Friendship.deleteOne({ owner: accountId, friend: friendId }),
Friendship.deleteOne({ owner: friendId, friend: accountId })
]);
res.json({
Friends: [{ $oid: friendId }]
} satisfies IRemoveFriendsResponse);
}
};
export const removeFriendPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IBatchRemoveFriendsRequest>(String(req.body));
const friends = new Set((await Friendship.find({ owner: accountId }, "friend")).map(x => x.friend));
// TOVERIFY: Should pending friendships also be kept?
// Keep friends that have been online within threshold
await parallelForeach([...friends], async friend => {
const account = (await Account.findById(friend, "LastLogin"))!;
const daysLoggedOut = (Date.now() - account.LastLogin.getTime()) / 86400_000;
if (daysLoggedOut < data.DaysLoggedOut) {
friends.delete(friend);
}
});
if (data.SkipClanmates) {
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
await parallelForeach([...friends], async friend => {
const friendInventory = await getInventory(friend.toString(), "GuildId");
if (friendInventory.GuildId?.equals(inventory.GuildId)) {
friends.delete(friend);
}
});
}
}
// Remove all remaining friends that aren't in SkipFriendIds & give response.
const promises = [];
const response: IOid[] = [];
for (const friend of friends) {
if (!data.SkipFriendIds.find(skipFriendId => checkFriendId(skipFriendId, friend))) {
promises.push(Friendship.deleteOne({ owner: accountId, friend: friend }));
promises.push(Friendship.deleteOne({ owner: friend, friend: accountId }));
response.push(toOid(friend));
}
}
await Promise.all(promises);
res.json({
Friends: response
} satisfies IRemoveFriendsResponse);
};
// The friend ids format is a bit weird, e.g. when 6633b81e9dba0b714f28ff02 (A) is friends with 67cdac105ef1f4b49741c267 (B), A's friend id for B is 808000105ef1f40560ca079e and B's friend id for A is 8000b81e9dba0b06408a8075.
const checkFriendId = (friendId: string, b: Types.ObjectId): boolean => {
return friendId.substring(6, 6 + 8) == b.toString().substring(6, 6 + 8);
};
interface IBatchRemoveFriendsRequest {
DaysLoggedOut: number;
SkipClanmates: boolean;
SkipFriendIds: string[];
}
interface IRemoveFriendsResponse {
Friends: IOid[];
}

View File

@ -1,23 +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 renamePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "KubrowPets PremiumCredits PremiumCreditsFree");
const data = getJSONfromString<IRenamePetRequest>(String(req.body));
const details = inventory.KubrowPets.id(data.petId)!.Details!;
details.Name = data.name;
const currencyChanges = updateCurrency(inventory, 15, true);
await inventory.save();
res.json({
...data,
inventoryChanges: currencyChanges
});
};
interface IRenamePetRequest {
petId: string;
name: string;
}

View File

@ -1,7 +1,8 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { addEmailItem, getDialogue, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -24,7 +25,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
inventory.DialogueHistory.Dialogues ??= []; inventory.DialogueHistory.Dialogues ??= [];
const dialogue = getDialogue(inventory, request.DialogueName); const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank; dialogue.Rank = request.Rank;
dialogue.Chemistry += request.Chemistry; dialogue.Chemistry = request.Chemistry;
dialogue.QueuedDialogues = request.QueuedDialogues; dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) { for (const bool of request.Booleans) {
dialogue.Booleans.push(bool); dialogue.Booleans.push(bool);
@ -106,3 +107,26 @@ interface IOtherDialogueInfo {
Tag: string; Tag: string;
Value: number; 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

@ -2,12 +2,11 @@ import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes"; import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService"; import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
export const saveLoadoutController: RequestHandler = async (req, res) => { export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = getJSONfromString<ISaveLoadoutRequest>(String(req.body)); const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true })); // console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars

View File

@ -45,9 +45,6 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) { if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS); requiredFields.add(InventorySlot.SPACEWEAPONS);
} }
if (payload.Items.MechSuits) {
requiredFields.add(InventorySlot.MECHSUITS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) { if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) {
requiredFields.add(InventorySlot.SENTINELS); requiredFields.add(InventorySlot.SENTINELS);
} }
@ -139,12 +136,6 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SPACEWEAPONS); freeUpSlot(inventory, InventorySlot.SPACEWEAPONS);
}); });
} }
if (payload.Items.MechSuits) {
payload.Items.MechSuits.forEach(sellItem => {
inventory.MechSuits.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.MECHSUITS);
});
}
if (payload.Items.Sentinels) { if (payload.Items.Sentinels) {
payload.Items.Sentinels.forEach(sellItem => { payload.Items.Sentinels.forEach(sellItem => {
inventory.Sentinels.pull({ _id: sellItem.String }); inventory.Sentinels.pull({ _id: sellItem.String });
@ -294,7 +285,6 @@ interface ISellRequest {
SpaceSuits?: ISellItem[]; SpaceSuits?: ISellItem[];
SpaceGuns?: ISellItem[]; SpaceGuns?: ISellItem[];
SpaceMelee?: ISellItem[]; SpaceMelee?: ISellItem[];
MechSuits?: ISellItem[];
Sentinels?: ISellItem[]; Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[]; SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[]; MoaPets?: ISellItem[];

View File

@ -13,7 +13,7 @@ export const setDojoComponentSettingsController: RequestHandler = async (req, re
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
const component = guild.DojoComponents.id(req.query.componentId as string)!; const component = guild.DojoComponents.id(req.query.componentId)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body)); const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings; component.Settings = data.Settings;
await guild.save(); await guild.save();

View File

@ -1,30 +0,0 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Friendship } from "@/src/models/friendModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const setFriendNoteController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<ISetFriendNoteRequest>(String(req.body));
const friendship = await Friendship.findOne({ owner: accountId, friend: payload.FriendId }, "Note Favorite");
if (friendship) {
if ("Note" in payload) {
friendship.Note = payload.Note;
} else {
friendship.Favorite = payload.Favorite;
}
await friendship.save();
}
res.json({
Id: payload.FriendId,
SetNote: "Note" in payload,
Note: friendship?.Note,
Favorite: friendship?.Favorite
});
};
interface ISetFriendNoteRequest {
FriendId: string;
Note?: string;
Favorite?: boolean;
}

View File

@ -1,8 +1,8 @@
import { version_compare } from "@/src/helpers/inventoryHelpers";
import { Alliance, Guild, GuildMember } from "@/src/models/guildModel"; import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermissionEx } from "@/src/services/guildService"; import { hasGuildPermissionEx } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { version_compare } from "@/src/services/worldStateService";
import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes"; import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";

View File

@ -11,7 +11,7 @@ import {
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
interface IStartDojoRecipeRequest { interface IStartDojoRecipeRequest {
@ -20,10 +20,10 @@ interface IStartDojoRecipeRequest {
} }
export const startDojoRecipeController: RequestHandler = async (req, res) => { export const startDojoRecipeController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys"); const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory); const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, account._id, GuildPermission.Architect))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Architect))) {
res.json({ DojoRequestStatus: -1 }); res.json({ DojoRequestStatus: -1 });
return; return;
} }
@ -64,5 +64,5 @@ export const startDojoRecipeController: RequestHandler = async (req, res) => {
setDojoRoomLogFunded(guild, component); setDojoRoomLogFunded(guild, component);
} }
await guild.save(); await guild.save();
res.json(await getDojoClient(guild, 0, undefined, account.BuildLabel)); res.json(await getDojoClient(guild, 0));
}; };

View File

@ -3,14 +3,12 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { addItem, addKubrowPet, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addItem, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot, ISpectreLoadout } from "@/src/types/inventoryTypes/inventoryTypes";
import { fromOid, toOid } from "@/src/helpers/inventoryHelpers"; import { toOid } from "@/src/helpers/inventoryHelpers";
import { ExportWeapons } from "warframe-public-export-plus"; import { ExportWeapons } from "warframe-public-export-plus";
import { getRandomElement } from "@/src/services/rngService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
interface IStartRecipeRequest { interface IStartRecipeRequest {
RecipeName: string; RecipeName: string;
@ -43,36 +41,25 @@ export const startRecipeController: RequestHandler = async (req, res) => {
]; ];
for (let i = 0; i != recipe.ingredients.length; ++i) { for (let i = 0; i != recipe.ingredients.length; ++i) {
if (startRecipeRequest.Ids[i] && startRecipeRequest.Ids[i][0] != "/") { if (startRecipeRequest.Ids[i]) {
if (recipe.ingredients[i].ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
const index = inventory.KubrowPetEggs!.findIndex(x => x._id.equals(startRecipeRequest.Ids[i])); if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
if (index != -1) { throw new Error(`unexpected equipment ingredient type: ${category}`);
inventory.KubrowPetEggs!.splice(index, 1);
}
} else {
const category = ExportWeapons[recipe.ingredients[i].ItemType].productCategory;
if (category != "LongGuns" && category != "Pistols" && category != "Melee") {
throw new Error(`unexpected equipment ingredient type: ${category}`);
}
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
freeUpSlot(inventory, InventorySlot.WEAPONS);
} }
const equipmentIndex = inventory[category].findIndex(x => x._id.equals(startRecipeRequest.Ids[i]));
if (equipmentIndex == -1) {
throw new Error(`could not find equipment item to use for recipe`);
}
pr[category] ??= [];
pr[category].push(inventory[category][equipmentIndex]);
inventory[category].splice(equipmentIndex, 1);
freeUpSlot(inventory, InventorySlot.WEAPONS);
} else { } else {
await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1); await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
} }
} }
let inventoryChanges: IInventoryChanges | undefined; if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
if (recipe.secretIngredientAction == "SIA_CREATE_KUBROW") {
inventoryChanges = addKubrowPet(inventory, getRandomElement(recipe.secretIngredients!)!.ItemType);
pr.KubrowPet = new Types.ObjectId(fromOid(inventoryChanges.KubrowPets![0].ItemId));
} else if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
const spectreLoadout: ISpectreLoadout = { const spectreLoadout: ISpectreLoadout = {
ItemType: recipe.resultType, ItemType: recipe.resultType,
Suits: "", Suits: "",
@ -129,5 +116,5 @@ export const startRecipeController: RequestHandler = async (req, res) => {
await inventory.save(); await inventory.save();
res.json({ RecipeId: toOid(pr._id), InventoryChanges: inventoryChanges }); res.json({ RecipeId: toOid(pr._id) });
}; };

View File

@ -1,13 +1,15 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItem, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { toStoreItem } from "@/src/services/itemDataService"; import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
export const syndicateSacrificeController: RequestHandler = async (request, response) => { export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
@ -18,83 +20,74 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1]; syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: data.AffiliationTag, Standing: 0 }) - 1];
} }
const oldLevel = syndicate.Title ?? 0; const level = data.SacrificeLevel - (syndicate.Title ?? 0);
const levelIncrease = data.SacrificeLevel - oldLevel;
if (levelIncrease < 0) {
throw new Error(`syndicate sacrifice can not decrease level`);
}
if (levelIncrease > 1 && !data.AllowMultiple) {
throw new Error(`desired syndicate level is an increase of ${levelIncrease}, max. allowed increase is 1`);
}
const res: ISyndicateSacrificeResponse = { const res: ISyndicateSacrificeResponse = {
AffiliationTag: data.AffiliationTag, AffiliationTag: data.AffiliationTag,
InventoryChanges: {}, InventoryChanges: {},
Level: data.SacrificeLevel, Level: data.SacrificeLevel,
LevelIncrease: data.SacrificeLevel < 0 ? 1 : levelIncrease, LevelIncrease: level <= 0 ? 1 : level,
NewEpisodeReward: false NewEpisodeReward: false
}; };
// Process sacrifices and rewards for every level we're reaching
const manifest = ExportSyndicates[data.AffiliationTag]; const manifest = ExportSyndicates[data.AffiliationTag];
for (let level = oldLevel + Math.min(levelIncrease, 1); level <= data.SacrificeLevel; ++level) { let sacrifice: ISyndicateSacrifice | undefined;
let sacrifice: ISyndicateSacrifice | undefined; let reward: string | undefined;
if (level == 0) { if (data.SacrificeLevel == 0) {
sacrifice = manifest.initiationSacrifice; sacrifice = manifest.initiationSacrifice;
if (manifest.initiationReward) { reward = manifest.initiationReward;
combineInventoryChanges( syndicate.Initiated = true;
res.InventoryChanges, } else {
(await handleStoreItemAcquisition(manifest.initiationReward, inventory)).InventoryChanges sacrifice = manifest.titles?.find(x => x.level == data.SacrificeLevel)?.sacrifice;
); }
}
syndicate.Initiated = true;
} else {
sacrifice = manifest.titles?.find(x => x.level == level)?.sacrifice;
}
if (sacrifice) { if (sacrifice) {
updateCurrency(inventory, sacrifice.credits, false, res.InventoryChanges); res.InventoryChanges = { ...updateCurrency(inventory, sacrifice.credits, false) };
for (const item of sacrifice.items) { const miscItemChanges = sacrifice.items.map(x => ({
addMiscItem(inventory, item.ItemType, item.ItemCount * -1, res.InventoryChanges); ItemType: x.ItemType,
} ItemCount: x.ItemCount * -1
} }));
addMiscItems(inventory, miscItemChanges);
res.InventoryChanges.MiscItems = miscItemChanges;
}
// Quacks like a nightwave syndicate? syndicate.Title ??= 0;
if (manifest.dailyChallenges) { syndicate.Title += 1;
const title = manifest.titles!.find(x => x.level == level);
if (title) { if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
res.NewEpisodeReward = true; syndicate.FreeFavorsEarned ??= [];
let rewardType: string; if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
let rewardCount: number; syndicate.FreeFavorsEarned.push(syndicate.Title);
if (title.storeItemReward) { }
rewardType = title.storeItemReward; }
rewardCount = 1;
} else { if (reward) {
rewardType = toStoreItem(title.reward!.ItemType); combineInventoryChanges(
rewardCount = title.reward!.ItemCount; res.InventoryChanges,
} (await handleStoreItemAcquisition(reward, inventory)).InventoryChanges
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, rewardCount)) );
.InventoryChanges; }
if (Object.keys(rewardInventoryChanges).length == 0) {
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`); if (data.AffiliationTag == ExportNightwave.affiliationTag) {
const nightwaveCredsItemType = manifest.titles![0].reward!.ItemType; const index = syndicate.Title - 1;
addMiscItem(inventory, nightwaveCredsItemType, 50, rewardInventoryChanges); if (index < ExportNightwave.rewards.length) {
} res.NewEpisodeReward = true;
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges); const reward = ExportNightwave.rewards[index];
} let rewardType = reward.uniqueName;
} else { if (!isStoreItem(rewardType)) {
if (level > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == level)) { rewardType = toStoreItem(rewardType);
syndicate.FreeFavorsEarned ??= []; }
if (!syndicate.FreeFavorsEarned.includes(level)) { const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount))
syndicate.FreeFavorsEarned.push(level); .InventoryChanges;
} if (Object.keys(rewardInventoryChanges).length == 0) {
} logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
addMiscItems(inventory, rewardInventoryChanges.MiscItems);
}
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
} }
} }
// Commit
syndicate.Title = data.SacrificeLevel < 0 ? data.SacrificeLevel + 1 : data.SacrificeLevel;
await inventory.save(); await inventory.save();
response.json(res); response.json(res);

View File

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

View File

@ -35,17 +35,6 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
inventory.PlayerLevel += 1; inventory.PlayerLevel += 1;
inventory.TradesRemaining += 1; inventory.TradesRemaining += 1;
if (inventory.PlayerLevel == 2) {
await createMessage(accountId, [
{
sndr: "/Lotus/Language/Game/Maroo",
msg: "/Lotus/Language/Clan/MarooClanSearchDesc",
sub: "/Lotus/Language/Clan/MarooClanSearchTitle",
icon: "/Lotus/Interface/Icons/Npcs/Maroo.png"
}
]);
}
await createMessage(accountId, [ await createMessage(accountId, [
{ {
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",

View File

@ -1,26 +1,18 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addChallenges, getInventory } from "@/src/services/inventoryService"; import { addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { IAffiliationMods } from "@/src/types/purchaseTypes"; import { IAffiliationMods } from "@/src/types/purchaseTypes";
export const updateChallengeProgressController: RequestHandler = async (req, res) => { export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body)); const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const account = await getAccountForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory( const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
account._id.toString(),
"ChallengeProgress SeasonChallengeHistory Affiliations"
);
let affiliationMods: IAffiliationMods[] = []; let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) { if (challenges.ChallengeProgress) {
affiliationMods = addChallenges( affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
account,
inventory,
challenges.ChallengeProgress,
challenges.SeasonChallengeCompletions
);
} }
if (challenges.SeasonChallengeHistory) { if (challenges.SeasonChallengeHistory) {
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => { challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {

View File

@ -1,6 +1,5 @@
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService"; import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -12,7 +11,7 @@ export const addXpController: RequestHandler = async (req, res) => {
const request = req.body as IAddXpRequest; const request = req.body as IAddXpRequest;
for (const [category, gear] of Object.entries(request)) { for (const [category, gear] of Object.entries(request)) {
for (const clientItem of gear) { for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id((clientItem.ItemId as IOid).$oid); const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid);
if (dbItem) { if (dbItem) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) { if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) { if ((dbItem.Polarized ?? 0) < 5) {

View File

@ -10,7 +10,6 @@ import { Stats } from "@/src/models/statsModel";
import { GuildMember } from "@/src/models/guildModel"; import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel"; import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService"; import { deleteGuild } from "@/src/services/guildService";
import { Friendship } from "@/src/models/friendModel";
export const deleteAccountController: RequestHandler = async (req, res) => { export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -23,8 +22,6 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
await Promise.all([ await Promise.all([
Account.deleteOne({ _id: accountId }), Account.deleteOne({ _id: accountId }),
Friendship.deleteMany({ owner: accountId }),
Friendship.deleteMany({ friend: accountId }),
GuildMember.deleteMany({ accountId: accountId }), GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }), Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }), Ignore.deleteMany({ ignoree: accountId }),

View File

@ -1,18 +1,15 @@
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel"; import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getAccountInfoController: RequestHandler = async (req, res) => { export const getAccountInfoController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "QuestKeys");
const info: IAccountInfo = { const info: IAccountInfo = {
DisplayName: account.DisplayName, DisplayName: account.DisplayName
IsAdministrator: isAdministrator(account),
CompletedVorsPrize: !!inventory.QuestKeys.find(
x => x.ItemType == "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"
)?.Completed
}; };
if (isAdministrator(account)) {
info.IsAdministrator = true;
}
const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank");
if (guildMember) { if (guildMember) {
const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!; const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!;
@ -34,8 +31,7 @@ export const getAccountInfoController: RequestHandler = async (req, res) => {
interface IAccountInfo { interface IAccountInfo {
DisplayName: string; DisplayName: string;
IsAdministrator: boolean; IsAdministrator?: boolean;
CompletedVorsPrize: boolean;
GuildId?: string; GuildId?: string;
GuildPermissions?: number; GuildPermissions?: number;
GuildRank?: number; GuildRank?: number;

View File

@ -3,7 +3,6 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
import { import {
ExportArcanes, ExportArcanes,
ExportAvionics, ExportAvionics,
ExportBoosters,
ExportCustoms, ExportCustoms,
ExportDrones, ExportDrones,
ExportGear, ExportGear,
@ -20,12 +19,12 @@ import {
ExportWeapons, ExportWeapons,
TRelicQuality TRelicQuality
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json"; import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
interface ListedItem { interface ListedItem {
uniqueName: string; uniqueName: string;
name: string; name: string;
subtype?: string;
fusionLimit?: number; fusionLimit?: number;
exalted?: string[]; exalted?: string[];
badReason?: "starter" | "frivolous" | "notraw"; badReason?: "starter" | "frivolous" | "notraw";
@ -35,6 +34,7 @@ interface ListedItem {
} }
interface ItemLists { interface ItemLists {
archonCrystalUpgrades: Record<string, string>;
uniqueLevelCaps: Record<string, number>; uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[]; Suits: ListedItem[];
LongGuns: ListedItem[]; LongGuns: ListedItem[];
@ -54,7 +54,6 @@ interface ItemLists {
KubrowPets: ListedItem[]; KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[]; EvolutionProgress: ListedItem[];
mods: ListedItem[]; mods: ListedItem[];
Boosters: ListedItem[];
} }
const relicQualitySuffixes: Record<TRelicQuality, string> = { const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -67,6 +66,7 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
const getItemListsController: RequestHandler = (req, response) => { const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en"); const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: ItemLists = { const res: ItemLists = {
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps, uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [], Suits: [],
LongGuns: [], LongGuns: [],
@ -85,8 +85,7 @@ const getItemListsController: RequestHandler = (req, response) => {
QuestKeys: [], QuestKeys: [],
KubrowPets: [], KubrowPets: [],
EvolutionProgress: [], EvolutionProgress: [],
mods: [], mods: []
Boosters: []
}; };
for (const [uniqueName, item] of Object.entries(ExportWarframes)) { for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({ res[item.productCategory].push({
@ -176,8 +175,7 @@ const getItemListsController: RequestHandler = (req, response) => {
) { ) {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: name, name: name
subtype: "Resource"
}); });
} }
} }
@ -195,8 +193,7 @@ const getItemListsController: RequestHandler = (req, response) => {
for (const [uniqueName, item] of Object.entries(ExportGear)) { for (const [uniqueName, item] of Object.entries(ExportGear)) {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: getString(item.name, lang), name: getString(item.name, lang)
subtype: "Gear"
}); });
} }
const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang); const recipeNameTemplate = getString("/Lotus/Language/Items/BlueprintAndItem", lang);
@ -296,13 +293,6 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
for (const item of Object.values(ExportBoosters)) {
res.Boosters.push({
uniqueName: item.typeName,
name: getString(item.name, lang)
});
}
response.json(res); response.json(res);
}; };

View File

@ -1,45 +0,0 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { ExportBoosters } from "warframe-public-export-plus";
const I32_MAX = 0x7fffffff;
export const setBoosterController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const requests = req.body as { ItemType: string; ExpiryDate: number }[];
const inventory = await getInventory(accountId, "Boosters");
const boosters = inventory.Boosters;
if (
requests.some(request => {
if (typeof request.ItemType !== "string") return true;
if (Object.entries(ExportBoosters).find(([_, item]) => item.typeName === request.ItemType) === undefined)
return true;
if (typeof request.ExpiryDate !== "number") return true;
if (request.ExpiryDate < 0 || request.ExpiryDate > I32_MAX) return true;
return false;
})
) {
res.status(400).send("Invalid ItemType provided.");
return;
}
const now = Math.floor(Date.now() / 1000);
for (const { ItemType, ExpiryDate } of requests) {
if (ExpiryDate < now) {
// remove expired boosters
const index = boosters.findIndex(item => item.ItemType === ItemType);
if (index !== -1) {
boosters.splice(index, 1);
}
} else {
const boosterItem = boosters.find(item => item.ItemType === ItemType);
if (boosterItem) {
boosterItem.ExpiryDate = ExpiryDate;
} else {
boosters.push({ ItemType, ExpiryDate });
}
}
}
await inventory.save();
res.end();
};

View File

@ -84,10 +84,7 @@ export const getProfileViewingDataGetController: RequestHandler = async (req, re
res.status(409).send("Could not find requested account"); res.status(409).send("Could not find requested account");
} }
} else if (req.query.guildId) { } else if (req.query.guildId) {
const guild = await Guild.findById( const guild = await Guild.findById(req.query.guildId, "Name Tier XP Class Emblem TechProjects ClaimedXP");
req.query.guildId as string,
"Name Tier XP Class Emblem TechProjects ClaimedXP"
);
if (!guild) { if (!guild) {
res.status(409).send("Could not find guild"); res.status(409).send("Could not find guild");
return; return;

View File

@ -48,8 +48,7 @@ const toDatabaseAccount = (createAccount: IAccountCreation): IDatabaseAccountReq
CrossPlatformAllowed: true, CrossPlatformAllowed: true,
ForceLogoutVersion: 0, ForceLogoutVersion: 0,
TrackedSettings: [], TrackedSettings: [],
Nonce: 0, Nonce: 0
LastLogin: new Date()
} satisfies IDatabaseAccountRequiredFields; } satisfies IDatabaseAccountRequiredFields;
}; };

View File

@ -1,46 +1,9 @@
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { TRarity } from "warframe-public-export-plus"; import { TRarity } from "warframe-public-export-plus";
export const version_compare = (a: string, b: string): number => {
const a_digits = a
.split("/")[0]
.split(".")
.map(x => parseInt(x));
const b_digits = b
.split("/")[0]
.split(".")
.map(x => parseInt(x));
for (let i = 0; i != a_digits.length; ++i) {
if (a_digits[i] != b_digits[i]) {
return a_digits[i] > b_digits[i] ? 1 : -1;
}
}
return 0;
};
export const toOid = (objectId: Types.ObjectId): IOid => { export const toOid = (objectId: Types.ObjectId): IOid => {
return { $oid: objectId.toString() }; return { $oid: objectId.toString() } satisfies IOid;
};
export function toOid2(objectId: Types.ObjectId, buildLabel: undefined): IOid;
export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport;
export function toOid2(objectId: Types.ObjectId, buildLabel: string | undefined): IOidWithLegacySupport {
if (buildLabel && version_compare(buildLabel, "2016.12.21.19.13") <= 0) {
return { $id: objectId.toString() };
}
return { $oid: objectId.toString() };
}
export const toLegacyOid = (oid: IOidWithLegacySupport): void => {
if (!("$id" in oid)) {
oid.$id = oid.$oid;
delete oid.$oid;
}
};
export const fromOid = (oid: IOidWithLegacySupport): string => {
return (oid.$oid ?? oid.$id)!;
}; };
export const toMongoDate = (date: Date): IMongoDate => { export const toMongoDate = (date: Date): IMongoDate => {

View File

@ -1,203 +1,18 @@
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus"; import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, TNemesisFaction } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { getRewardAtPercentage, SRng } from "@/src/services/rngService"; import { getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { IOid } from "../types/commonTypes"; import { IOid } from "../types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { addMods, generateRewardSeed } from "../services/inventoryService"; import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission } from "../services/worldStateService"; import { isArchwingMission, version_compare } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
type TInnateDamageTag = export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
| "InnateElectricityDamage"
| "InnateHeatDamage"
| "InnateFreezeDamage"
| "InnateToxinDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateImpactDamage";
export interface INemesisManifest {
weapons: readonly string[];
systemIndexes: readonly number[];
showdownNode: string;
ephemeraChance: number;
ephemeraTypes?: Record<TInnateDamageTag, string>;
firstKillReward: string;
firstConvertReward: string;
messageTitle: string;
messageBody: string;
minBuild: string;
}
class KuvaLichManifest implements INemesisManifest {
weapons = [
"/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"
];
systemIndexes = [2, 3, 9, 11, 18];
showdownNode = "CrewBattleNode557";
ephemeraChance = 0.05;
ephemeraTypes = {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaLightningEphemera",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaFireEphemera",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaIceEphemera",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaToxinEphemera",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaMagneticEphemera",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaTricksterEphemera",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/Kuva/KuvaImpactEphemera"
};
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/LichKillerBadgeItem";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/KuvaLichSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishKuvaMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
minBuild = "2019.10.31.22.42"; // 26.0.0
}
class KuvaLichManifestVersionTwo extends KuvaLichManifest {
constructor() {
super();
this.ephemeraChance = 0.1;
this.minBuild = "2020.03.05.16.06"; // Unsure about this one, so using the same value as in version three.
}
}
class KuvaLichManifestVersionThree extends KuvaLichManifestVersionTwo {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor");
this.ephemeraChance = 0.2;
this.minBuild = "2020.03.05.16.06"; // This is 27.2.0, tho 27.1.0 should also recognise this.
}
}
class KuvaLichManifestVersionFour extends KuvaLichManifestVersionThree {
constructor() {
super();
this.minBuild = "2021.07.05.17.03"; // Unsure about this one, so using the same value as in version five.
}
}
class KuvaLichManifestVersionFive extends KuvaLichManifestVersionFour {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr");
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler");
this.minBuild = "2021.07.05.17.03"; // 30.5.0
}
}
class KuvaLichManifestVersionSix extends KuvaLichManifestVersionFive {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek");
this.minBuild = "2024.05.15.11.07"; // 35.6.0
}
}
class LawyerManifest implements INemesisManifest {
weapons = [
"/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"
];
systemIndexes = [1, 15, 4, 7, 8];
showdownNode = "CrewBattleNode558";
ephemeraChance = 0.2;
ephemeraTypes = {
InnateElectricityDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraA",
InnateHeatDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraB",
InnateFreezeDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraC",
InnateToxinDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraD",
InnateMagDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraE",
InnateRadDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraF",
InnateImpactDamage: "/Lotus/Upgrades/Skins/Effects/CorpusLichEphemeraG"
};
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/CorpusLichBadgeItem";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/CorpusLichSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishLawyerMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
minBuild = "2021.07.05.17.03"; // 30.5.0
}
class LawyerManifestVersionTwo extends LawyerManifest {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon");
this.minBuild = "2022.11.30.08.13"; // 32.2.0
}
}
class LawyerManifestVersionThree extends LawyerManifestVersionTwo {
constructor() {
super();
this.weapons.push("/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion");
this.minBuild = "2024.05.15.11.07"; // 35.6.0
}
}
class LawyerManifestVersionFour extends LawyerManifestVersionThree {
constructor() {
super();
this.minBuild = "2024.10.01.11.03"; // 37.0.0
}
}
class InfestedLichManfest implements INemesisManifest {
weapons = [];
systemIndexes = [23];
showdownNode = "CrewBattleNode559";
ephemeraChance = 0;
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichVanquishedSigil";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichConvertedSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishBandMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishBandMsgBody";
minBuild = "2025.03.18.09.51"; // 38.5.0
}
const nemesisManifests: Record<string, INemesisManifest> = {
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifest": new KuvaLichManifest(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionTwo": new KuvaLichManifestVersionTwo(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionThree": new KuvaLichManifestVersionThree(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionFour": new KuvaLichManifestVersionFour(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionFive": new KuvaLichManifestVersionFive(),
"/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": new KuvaLichManifestVersionSix(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifest": new LawyerManifest(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionTwo": new LawyerManifestVersionTwo(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": new LawyerManifestVersionThree(),
"/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": new LawyerManifestVersionFour(),
"/Lotus/Types/Enemies/InfestedLich/InfestedLichManifest": new InfestedLichManfest()
};
export const getNemesisManifest = (manifest: string): INemesisManifest => {
if (manifest in nemesisManifests) {
return nemesisManifests[manifest];
}
throw new Error(`unknown nemesis manifest: ${manifest}`);
};
export const getInfNodes = (manifest: INemesisManifest, rank: number): IInfNode[] => {
const infNodes = []; const infNodes = [];
const systemIndex = manifest.systemIndexes[rank]; const systemIndex = systemIndexes[faction][rank];
for (const [key, value] of Object.entries(ExportRegions)) { for (const [key, value] of Object.entries(ExportRegions)) {
if ( if (
value.systemIndex === systemIndex && value.systemIndex === systemIndex &&
@ -219,8 +34,20 @@ export const getInfNodes = (manifest: INemesisManifest, rank: number): IInfNode[
return infNodes; return infNodes;
}; };
const systemIndexes: Record<string, number[]> = {
FC_GRINEER: [2, 3, 9, 11, 18],
FC_CORPUS: [1, 15, 4, 7, 8],
FC_INFESTATION: [23]
};
export const showdownNodes: Record<string, string> = {
FC_GRINEER: "CrewBattleNode557",
FC_CORPUS: "CrewBattleNode558",
FC_INFESTATION: "CrewBattleNode559"
};
// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis. // Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis.
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFaction }): number[] => { export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => {
const rng = new SRng(nemesis.fp); const rng = new SRng(nemesis.fp);
const choices = [0, 1, 2, 3, 5, 6, 7]; const choices = [0, 1, 2, 3, 5, 6, 7];
let choiceIndex = rng.randomInt(0, choices.length - 1); let choiceIndex = rng.randomInt(0, choices.length - 1);
@ -237,7 +64,7 @@ export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: TNemesisFacti
return passcode; return passcode;
}; };
const requiemMods: readonly string[] = [ const reqiuemMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod", "/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
@ -259,55 +86,33 @@ const antivirusMods: readonly string[] = [
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod" "/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
]; ];
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: TNemesisFaction }): string[] => { export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => {
const passcode = getNemesisPasscode(nemesis); const passcode = getNemesisPasscode(nemesis);
return nemesis.Faction == "FC_INFESTATION" return nemesis.Faction == "FC_INFESTATION"
? passcode.map(i => antivirusMods[i]) ? passcode.map(i => antivirusMods[i])
: passcode.map(i => requiemMods[i]); : passcode.map(i => reqiuemMods[i]);
}; };
// Symbols; 0-7 are the normal requiem mods. export const encodeNemesisGuess = (
export const GUESS_NONE = 8; symbol1: number,
export const GUESS_WILDCARD = 9; result1: number,
symbol2: number,
// Results; there are 3, 4, 5 as well which are more muted versions but unused afaik. result2: number,
export const GUESS_NEUTRAL = 0; symbol3: number,
export const GUESS_INCORRECT = 1; result3: number
export const GUESS_CORRECT = 2; ): number => {
interface NemesisPositionGuess {
symbol: number;
result: number;
}
export type NemesisGuess = [NemesisPositionGuess, NemesisPositionGuess, NemesisPositionGuess];
export const encodeNemesisGuess = (guess: NemesisGuess): number => {
return ( return (
(guess[0].symbol & 0xf) | (symbol1 & 0xf) |
((guess[0].result & 3) << 12) | ((result1 & 3) << 12) |
((guess[1].symbol << 4) & 0xff) | ((symbol2 << 4) & 0xff) |
((guess[1].result << 14) & 0xffff) | ((result2 << 14) & 0xffff) |
((guess[2].symbol & 0xf) << 8) | ((symbol3 & 0xf) << 8) |
((guess[2].result & 3) << 16) ((result3 & 3) << 16)
); );
}; };
export const decodeNemesisGuess = (val: number): NemesisGuess => { export const decodeNemesisGuess = (val: number): number[] => {
return [ return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
{
symbol: val & 0xf,
result: (val >> 12) & 3
},
{
symbol: (val & 0xff) >> 4,
result: (val & 0xffff) >> 14
},
{
symbol: (val >> 8) & 0xf,
result: (val >> 16) & 3
}
];
}; };
export interface IKnifeResponse { export interface IKnifeResponse {
@ -394,31 +199,92 @@ export const consumeModCharge = (
} }
}; };
export const getInnateDamageTag = (KillingSuit: string): TInnateDamageTag => { const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];
export const getWeaponsForManifest = (manifest: string): readonly string[] => {
switch (manifest) {
case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix": // >= 35.6.0
return kuvaLichVersionSixWeapons;
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree": // >= 35.6.0
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour": // >= 37.0.0
return corpusVersionThreeWeapons;
}
throw new Error(`unknown nemesis manifest: ${manifest}`);
};
export const isNemesisCompatibleWithVersion = (
nemesis: { manifest: string; Faction: string },
buildLabel: string
): boolean => {
// Anything below 35.6.0 is not going to be okay given our set of supported manifests.
if (version_compare(buildLabel, "2024.05.15.11.07") < 0) {
return false;
}
if (nemesis.Faction == "FC_INFESTATION") {
// Anything below 38.5.0 isn't gonna like an infested lich.
if (version_compare(buildLabel, "2025.03.18.16.07") < 0) {
return false;
}
} else if (nemesis.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour") {
// Anything below 37.0.0 isn't gonna know version 4, but version 3 is identical in terms of weapon choices, so we can spoof it to that.
if (version_compare(buildLabel, "2024.10.01.11.03") < 0) {
nemesis.manifest = "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree";
}
}
return true;
};
export const getInnateDamageTag = (
KillingSuit: string
):
| "InnateElectricityDamage"
| "InnateFreezeDamage"
| "InnateHeatDamage"
| "InnateImpactDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateToxinDamage" => {
return ExportWarframes[KillingSuit].nemesisUpgradeTag!; return ExportWarframes[KillingSuit].nemesisUpgradeTag!;
}; };
const petHeads = [ // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA", export const getInnateDamageValue = (fp: bigint): number => {
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
] as const;
export interface INemesisProfile {
innateDamageTag: TInnateDamageTag;
innateDamageValue: number;
ephemera?: string;
petHead?: (typeof petHeads)[number];
petBody?: string;
petLegs?: string;
petTail?: string;
}
export const generateNemesisProfile = (
fp: bigint = generateRewardSeed(),
manifest: INemesisManifest = new LawyerManifest(),
killingSuit: string = "/Lotus/Powersuits/Ember/Ember"
): INemesisProfile => {
const rng = new SRng(fp); const rng = new SRng(fp);
rng.randomFloat(); // used for the weapon index rng.randomFloat(); // used for the weapon index
const WeaponUpgradeValueAttenuationExponent = 2.25; const WeaponUpgradeValueAttenuationExponent = 2.25;
@ -426,33 +292,7 @@ export const generateNemesisProfile = (
if (value >= 0.941428) { if (value >= 0.941428) {
value = 1; value = 1;
} }
const profile: INemesisProfile = { return Math.trunc(value * 0x40000000);
innateDamageTag: getInnateDamageTag(killingSuit),
innateDamageValue: Math.trunc(value * 0x40000000) // TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
};
if (rng.randomFloat() <= manifest.ephemeraChance && manifest.ephemeraTypes) {
profile.ephemera = manifest.ephemeraTypes[profile.innateDamageTag];
}
rng.randomFloat(); // something related to sentinel agent maybe
if (manifest instanceof LawyerManifest) {
profile.petHead = rng.randomElement(petHeads)!;
profile.petBody = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
profile.petLegs = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
profile.petTail = rng.randomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
}
return profile;
}; };
export const getKillTokenRewardCount = (fp: bigint): number => { export const getKillTokenRewardCount = (fp: bigint): number => {
@ -511,3 +351,52 @@ export const getInfestedLichItemRewards = (fp: bigint): string[] => {
const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type; const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
return [rotAReward, rotBReward]; return [rotAReward, rotBReward];
}; };
export const sendCodaFinishedMessage = async (
inventory: TInventoryDatabaseDocument,
fp: bigint = generateRewardSeed(),
name: string = "ZEKE_BEATWOMAN_TM.1999",
killed: boolean = true
): Promise<void> => {
const att: string[] = [];
// First vanquish/convert gives a sigil
const sigil = killed
? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil"
: "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil";
if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) {
att.push(toStoreItem(sigil));
}
const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp);
att.push(fromStoreItem(rotAReward));
att.push(fromStoreItem(rotBReward));
let countedAtt: ITypeCount[] | undefined;
if (killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(fp)
}
];
}
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/VanquishBandMsgBody",
arg: [
{
Key: "LICH_NAME",
Tag: name
}
],
att: att,
countedAtt: countedAtt,
sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
};

View File

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

View File

@ -2,7 +2,7 @@ import { JSONParse } from "json-with-bigint";
export const getJSONfromString = <T>(str: string): T => { export const getJSONfromString = <T>(str: string): T => {
const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1); const jsonSubstring = str.substring(0, str.lastIndexOf("}") + 1);
return JSONParse(jsonSubstring) as T; return JSONParse<T>(jsonSubstring);
}; };
export const getSubstringFromKeyword = (str: string, keyword: string): string => { export const getSubstringFromKeyword = (str: string, keyword: string): string => {

View File

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

View File

@ -1,14 +1,9 @@
// First, init config. // First, init config.
import { config, loadConfig } from "@/src/services/configService"; import { config, loadConfig } from "@/src/services/configService";
import fs from "fs";
try { try {
loadConfig(); loadConfig();
} catch (e) { } catch (e) {
if (fs.existsSync("config.json")) { console.log("ERROR: Failed to load config.json. You can copy config.json.example to create your config.json.");
console.log("Failed to load config.json: " + (e as Error).message);
} else {
console.log("Failed to load config.json. You can copy config.json.example to create your config.json.");
}
process.exit(1); process.exit(1);
} }
@ -17,14 +12,18 @@ import { logger } from "@/src/utils/logger";
logger.info("Starting up..."); logger.info("Starting up...");
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP. // Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
import http from "http";
import https from "https";
import fs from "node:fs";
import { app } from "./app";
import mongoose from "mongoose"; import mongoose from "mongoose";
import { JSONStringify } from "json-with-bigint"; import { Json, JSONStringify } from "json-with-bigint";
import { startWebServer } from "./services/webService";
import { validateConfig } from "@/src/services/configWatcherService"; import { validateConfig } from "@/src/services/configWatcherService";
// Patch JSON.stringify to work flawlessly with Bigints. // Patch JSON.stringify to work flawlessly with Bigints.
JSON.stringify = JSONStringify; JSON.stringify = (obj: Exclude<Json, undefined>, _replacer?: unknown, space?: string | number): string => {
return JSONStringify(obj, space);
};
validateConfig(); validateConfig();
@ -32,7 +31,26 @@ mongoose
.connect(config.mongodbUrl) .connect(config.mongodbUrl)
.then(() => { .then(() => {
logger.info("Connected to MongoDB"); logger.info("Connected to MongoDB");
startWebServer();
const httpPort = config.httpPort || 80;
const httpsPort = config.httpsPort || 443;
const options = {
key: fs.readFileSync("static/certs/key.pem"),
cert: fs.readFileSync("static/certs/cert.pem")
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
http.createServer(app).listen(httpPort, () => {
logger.info("HTTP server started on port " + httpPort);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
https.createServer(options, app).listen(httpsPort, () => {
logger.info("HTTPS server started on port " + httpsPort);
logger.info(
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
);
});
});
}) })
.catch(error => { .catch(error => {
if (error instanceof Error) { if (error instanceof Error) {

View File

@ -1,12 +1,10 @@
import { ISession, IFindSessionRequest } from "@/src/types/session"; import { ISession, IFindSessionRequest } from "@/src/types/session";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { JSONParse } from "json-with-bigint";
import { Types } from "mongoose";
const sessions: ISession[] = []; const sessions: ISession[] = [];
function createNewSession(sessionData: ISession, Creator: Types.ObjectId): ISession { function createNewSession(sessionData: ISession, Creator: string): ISession {
const sessionId = new Types.ObjectId(); const sessionId = getNewSessionID();
const newSession: ISession = { const newSession: ISession = {
sessionId, sessionId,
creatorId: Creator, creatorId: Creator,
@ -27,7 +25,7 @@ function createNewSession(sessionData: ISession, Creator: Types.ObjectId): ISess
customSettings: sessionData.customSettings || "", customSettings: sessionData.customSettings || "",
rewardSeed: sessionData.rewardSeed || -1, rewardSeed: sessionData.rewardSeed || -1,
guildId: sessionData.guildId || "", guildId: sessionData.guildId || "",
buildId: sessionData.buildId || 4920386201513015989n, buildId: sessionData.buildId || 4920386201513015989,
platform: sessionData.platform || 0, platform: sessionData.platform || 0,
xplatform: sessionData.xplatform || true, xplatform: sessionData.xplatform || true,
freePublic: sessionData.freePublic || 3, freePublic: sessionData.freePublic || 3,
@ -42,15 +40,13 @@ function getAllSessions(): ISession[] {
return sessions; return sessions;
} }
function getSessionByID(sessionId: string | Types.ObjectId): ISession | undefined { function getSessionByID(sessionId: string): ISession | undefined {
return sessions.find(session => session.sessionId.equals(sessionId)); return sessions.find(session => session.sessionId === sessionId);
} }
function getSession( function getSession(sessionIdOrRequest: string | IFindSessionRequest): { createdBy: string; id: string }[] {
sessionIdOrRequest: string | Types.ObjectId | IFindSessionRequest if (typeof sessionIdOrRequest === "string") {
): { createdBy: Types.ObjectId; id: Types.ObjectId }[] { const session = sessions.find(session => session.sessionId === sessionIdOrRequest);
if (typeof sessionIdOrRequest === "string" || sessionIdOrRequest instanceof Types.ObjectId) {
const session = sessions.find(session => session.sessionId.equals(sessionIdOrRequest));
if (session) { if (session) {
logger.debug("Found Sessions:", { session }); logger.debug("Found Sessions:", { session });
return [ return [
@ -83,15 +79,35 @@ function getSession(
})); }));
} }
function getSessionByCreatorID(creatorId: string | Types.ObjectId): ISession | undefined { function getSessionByCreatorID(creatorId: string): ISession | undefined {
return sessions.find(session => session.creatorId.equals(creatorId)); return sessions.find(session => session.creatorId === creatorId);
} }
function updateSession(sessionId: string | Types.ObjectId, sessionData: string): boolean { function getNewSessionID(): string {
const session = sessions.find(session => session.sessionId.equals(sessionId)); const characters = "0123456789abcdef";
const maxAttempts = 100;
let sessionId = "";
for (let attempt = 0; attempt < maxAttempts; attempt++) {
sessionId = "64";
for (let i = 0; i < 22; i++) {
const randomIndex = Math.floor(Math.random() * characters.length);
sessionId += characters[randomIndex];
}
if (!sessions.some(session => session.sessionId === sessionId)) {
return sessionId;
}
}
throw new Error("Failed to generate a unique session ID");
}
function updateSession(sessionId: string, sessionData: string): boolean {
const session = sessions.find(session => session.sessionId === sessionId);
if (!session) return false; if (!session) return false;
try { try {
Object.assign(session, JSONParse(sessionData)); Object.assign(session, JSON.parse(sessionData));
return true; return true;
} catch (error) { } catch (error) {
console.error("Invalid JSON string for session update."); console.error("Invalid JSON string for session update.");
@ -99,8 +115,8 @@ function updateSession(sessionId: string | Types.ObjectId, sessionData: string):
} }
} }
function deleteSession(sessionId: string | Types.ObjectId): boolean { function deleteSession(sessionId: string): boolean {
const index = sessions.findIndex(session => session.sessionId.equals(sessionId)); const index = sessions.findIndex(session => session.sessionId === sessionId);
if (index !== -1) { if (index !== -1) {
sessions.splice(index, 1); sessions.splice(index, 1);
return true; return true;
@ -113,6 +129,7 @@ export {
getAllSessions, getAllSessions,
getSessionByID, getSessionByID,
getSessionByCreatorID, getSessionByCreatorID,
getNewSessionID,
updateSession, updateSession,
deleteSession, deleteSession,
getSession getSession

View File

@ -1,15 +0,0 @@
import { IFriendship } from "@/src/types/friendTypes";
import { model, Schema } from "mongoose";
const friendshipSchema = new Schema<IFriendship>({
owner: { type: Schema.Types.ObjectId, required: true },
friend: { type: Schema.Types.ObjectId, required: true },
Note: String,
Favorite: Boolean
});
friendshipSchema.index({ owner: 1 });
friendshipSchema.index({ friend: 1 });
friendshipSchema.index({ owner: 1, friend: 1 }, { unique: true });
export const Friendship = model<IFriendship>("Friendship", friendshipSchema);

View File

@ -13,8 +13,7 @@ import {
IDojoLeaderboardEntry, IDojoLeaderboardEntry,
IGuildAdDatabase, IGuildAdDatabase,
IAllianceDatabase, IAllianceDatabase,
IAllianceMemberDatabase, IAllianceMemberDatabase
GuildPermission
} from "@/src/types/guildTypes"; } from "@/src/types/guildTypes";
import { Document, Model, model, Schema, Types } from "mongoose"; import { Document, Model, model, Schema, Types } from "mongoose";
import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel"; import { fusionTreasuresSchema, typeCountSchema } from "./inventoryModels/inventoryModel";
@ -109,31 +108,31 @@ const defaultRanks: IGuildRank[] = [
}, },
{ {
Name: "/Lotus/Language/Game/Rank_General", Name: "/Lotus/Language/Game/Rank_General",
Permissions: GuildPermission.Host | 4318 Permissions: 4318
}, },
{ {
Name: "/Lotus/Language/Game/Rank_Officer", Name: "/Lotus/Language/Game/Rank_Officer",
Permissions: GuildPermission.Host | 4314 Permissions: 4314
}, },
{ {
Name: "/Lotus/Language/Game/Rank_Leader", Name: "/Lotus/Language/Game/Rank_Leader",
Permissions: GuildPermission.Host | 4106 Permissions: 4106
}, },
{ {
Name: "/Lotus/Language/Game/Rank_Sage", Name: "/Lotus/Language/Game/Rank_Sage",
Permissions: GuildPermission.Host | 4304 Permissions: 4304
}, },
{ {
Name: "/Lotus/Language/Game/Rank_Soldier", Name: "/Lotus/Language/Game/Rank_Soldier",
Permissions: GuildPermission.Host | 4098 Permissions: 4098
}, },
{ {
Name: "/Lotus/Language/Game/Rank_Initiate", Name: "/Lotus/Language/Game/Rank_Initiate",
Permissions: GuildPermission.Host | GuildPermission.Fabricator Permissions: 4096
}, },
{ {
Name: "/Lotus/Language/Game/Rank_Utility", Name: "/Lotus/Language/Game/Rank_Utility",
Permissions: GuildPermission.Host | GuildPermission.Fabricator Permissions: 4096
} }
]; ];

View File

@ -38,8 +38,7 @@ import {
IPeriodicMissionCompletionResponse, IPeriodicMissionCompletionResponse,
ILoreFragmentScan, ILoreFragmentScan,
IEvolutionProgress, IEvolutionProgress,
IEndlessXpProgressDatabase, IEndlessXpProgress,
IEndlessXpProgressClient,
ICrewShipCustomization, ICrewShipCustomization,
ICrewShipWeapon, ICrewShipWeapon,
ICrewShipWeaponEmplacements, ICrewShipWeaponEmplacements,
@ -98,8 +97,7 @@ import {
IInvasionProgressClient, IInvasionProgressClient,
IAccolades, IAccolades,
IHubNpcCustomization, IHubNpcCustomization,
ILotusCustomization, ILotusCustomization
IEndlessXpReward
} from "../../types/inventoryTypes/inventoryTypes"; } from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes"; import { IOid } from "../../types/commonTypes";
import { import {
@ -114,7 +112,6 @@ import {
} from "@/src/types/inventoryTypes/commonInventoryTypes"; } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel"; import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel";
import { ICountedStoreItem } from "warframe-public-export-plus";
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false }); export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
@ -813,48 +810,14 @@ const evolutionProgressSchema = new Schema<IEvolutionProgress>(
{ _id: false } { _id: false }
); );
const countedStoreItemSchema = new Schema<ICountedStoreItem>( const endlessXpProgressSchema = new Schema<IEndlessXpProgress>(
{ {
StoreItem: String, Category: String,
ItemCount: Number Choices: [String]
}, },
{ _id: false } { _id: false }
); );
const endlessXpRewardSchema = new Schema<IEndlessXpReward>(
{
RequiredTotalXp: Number,
Rewards: [countedStoreItemSchema]
},
{ _id: false }
);
const endlessXpProgressSchema = new Schema<IEndlessXpProgressDatabase>(
{
Category: { type: String, required: true },
Earn: { type: Number, default: 0 },
Claim: { type: Number, default: 0 },
BonusAvailable: Date,
Expiry: Date,
Choices: { type: [String], required: true },
PendingRewards: { type: [endlessXpRewardSchema], default: [] }
},
{ _id: false }
);
endlessXpProgressSchema.set("toJSON", {
transform(_doc, ret) {
const db = ret as IEndlessXpProgressDatabase;
const client = ret as IEndlessXpProgressClient;
if (db.BonusAvailable) {
client.BonusAvailable = toMongoDate(db.BonusAvailable);
}
if (db.Expiry) {
client.Expiry = toMongoDate(db.Expiry);
}
}
});
const crewShipWeaponEmplacementsSchema = new Schema<ICrewShipWeaponEmplacements>( const crewShipWeaponEmplacementsSchema = new Schema<ICrewShipWeaponEmplacements>(
{ {
PRIMARY_A: EquipmentSelectionSchema, PRIMARY_A: EquipmentSelectionSchema,
@ -1097,8 +1060,7 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
LongGuns: { type: [EquipmentSchema], default: undefined }, LongGuns: { type: [EquipmentSchema], default: undefined },
Pistols: { type: [EquipmentSchema], default: undefined }, Pistols: { type: [EquipmentSchema], default: undefined },
Melee: { type: [EquipmentSchema], default: undefined }, Melee: { type: [EquipmentSchema], default: undefined },
SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }, SuitToUnbrand: { type: Schema.Types.ObjectId, default: undefined }
KubrowPet: { type: Schema.Types.ObjectId, default: undefined }
}, },
{ id: false } { id: false }
); );
@ -1116,7 +1078,6 @@ pendingRecipeSchema.set("toJSON", {
delete returnedObject.Pistols; delete returnedObject.Pistols;
delete returnedObject.Melees; delete returnedObject.Melees;
delete returnedObject.SuitToUnbrand; delete returnedObject.SuitToUnbrand;
delete returnedObject.KubrowPet;
(returnedObject as IPendingRecipeClient).CompletionDate = { (returnedObject as IPendingRecipeClient).CompletionDate = {
$date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() } $date: { $numberLong: (returnedObject as IPendingRecipeDatabase).CompletionDate.getTime().toString() }
}; };
@ -1320,7 +1281,7 @@ const nemesisSchema = new Schema<INemesisDatabase>(
InfNodes: { type: [infNodeSchema], default: undefined }, InfNodes: { type: [infNodeSchema], default: undefined },
HenchmenKilled: Number, HenchmenKilled: Number,
HintProgress: Number, HintProgress: Number,
Hints: { type: [Number], default: [] }, Hints: { type: [Number], default: undefined },
GuessHistory: { type: [Number], default: undefined }, GuessHistory: { type: [Number], default: undefined },
MissionCount: Number, MissionCount: Number,
LastEnc: Number LastEnc: Number
@ -1623,7 +1584,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
Drones: [droneSchema], Drones: [droneSchema],
//Active profile ico //Active profile ico
ActiveAvatarImageType: String, ActiveAvatarImageType: { type: String, default: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault" },
// open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable // open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable
DiscoveredMarkers: [discoveredMarkerSchema], DiscoveredMarkers: [discoveredMarkerSchema],

View File

@ -22,7 +22,6 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
Nonce: { type: Number, default: 0 }, Nonce: { type: Number, default: 0 },
BuildLabel: String, BuildLabel: String,
Dropped: Boolean, Dropped: Boolean,
LastLogin: { type: Date, default: 0 },
LatestEventMessageDate: { type: Date, default: 0 }, LatestEventMessageDate: { type: Date, default: 0 },
LastLoginRewardDate: { type: Number, default: 0 }, LastLoginRewardDate: { type: Number, default: 0 },
LoginDays: { type: Number, default: 1 } LoginDays: { type: Number, default: 1 }

View File

@ -13,7 +13,7 @@ import {
IPlantDatabase, IPlantDatabase,
IPlantClient IPlantClient
} from "@/src/types/shipTypes"; } from "@/src/types/shipTypes";
import { Schema, Types, model } from "mongoose"; import { Schema, model } from "mongoose";
export const pictureFrameInfoSchema = new Schema<IPictureFrameInfo>( export const pictureFrameInfoSchema = new Schema<IPictureFrameInfo>(
{ {
@ -153,18 +153,7 @@ const orbiterDefault: IOrbiter = {
Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear
Rooms: [ Rooms: [
{ Name: "AlchemyRoom", MaxCapacity: 1600 }, { Name: "AlchemyRoom", MaxCapacity: 1600 },
{ { Name: "BridgeRoom", MaxCapacity: 1600 },
Name: "BridgeRoom",
MaxCapacity: 1600,
PlacedDecos: [
{
Type: "/Lotus/Objects/Tenno/Props/Ships/LandCraftPlayerProps/ConclaveConsolePlayerShipDeco",
Pos: [-30.082, -3.95954, -16.7913],
Rot: [-135, 0, 0],
_id: undefined as unknown as Types.ObjectId
}
]
},
{ Name: "LisetRoom", MaxCapacity: 1000 }, { Name: "LisetRoom", MaxCapacity: 1000 },
{ Name: "OperatorChamberRoom", MaxCapacity: 1600 }, { Name: "OperatorChamberRoom", MaxCapacity: 1600 },
{ Name: "OutsideRoom", MaxCapacity: 1600 }, { Name: "OutsideRoom", MaxCapacity: 1600 },

View File

@ -28,15 +28,17 @@ shipSchema.set("toJSON", {
delete returnedObject._id; delete returnedObject._id;
delete returnedObject.__v; delete returnedObject.__v;
delete returnedObject.ShipOwnerId; delete returnedObject.ShipOwnerId;
if (shipDatabase.ShipExteriorColors) {
shipResponse.ShipExterior = {
Colors: shipDatabase.ShipExteriorColors,
ShipAttachments: shipDatabase.ShipAttachments,
SkinFlavourItem: shipDatabase.SkinFlavourItem
};
shipResponse.ShipExterior = { delete shipDatabase.ShipExteriorColors;
Colors: shipDatabase.ShipExteriorColors, delete shipDatabase.ShipAttachments;
ShipAttachments: shipDatabase.ShipAttachments, delete shipDatabase.SkinFlavourItem;
SkinFlavourItem: shipDatabase.SkinFlavourItem }
};
delete shipDatabase.ShipExteriorColors;
delete shipDatabase.ShipAttachments;
delete shipDatabase.SkinFlavourItem;
} }
}); });

View File

@ -3,13 +3,10 @@ import { abandonLibraryDailyTaskController } from "@/src/controllers/api/abandon
import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController"; import { abortDojoComponentController } from "@/src/controllers/api/abortDojoComponentController";
import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController"; import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController";
import { activateRandomModController } from "@/src/controllers/api/activateRandomModController"; import { activateRandomModController } from "@/src/controllers/api/activateRandomModController";
import { addFriendController } from "@/src/controllers/api/addFriendController";
import { addFriendImageController } from "@/src/controllers/api/addFriendImageController"; import { addFriendImageController } from "@/src/controllers/api/addFriendImageController";
import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController"; import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController";
import { addPendingFriendController } from "@/src/controllers/api/addPendingFriendController";
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController"; import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
import { addToGuildController } from "@/src/controllers/api/addToGuildController"; import { addToGuildController } from "@/src/controllers/api/addToGuildController";
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController"; import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
import { archonFusionController } from "@/src/controllers/api/archonFusionController"; import { archonFusionController } from "@/src/controllers/api/archonFusionController";
import { artifactsController } from "@/src/controllers/api/artifactsController"; import { artifactsController } from "@/src/controllers/api/artifactsController";
@ -100,15 +97,12 @@ import { playerSkillsController } from "@/src/controllers/api/playerSkillsContro
import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController"; import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController"; import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
import { purchaseController } from "@/src/controllers/api/purchaseController"; import { purchaseController } from "@/src/controllers/api/purchaseController";
import { questControlController } from "@/src/controllers/api/questControlController";
import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController"; import { queueDojoComponentDestructionController } from "@/src/controllers/api/queueDojoComponentDestructionController";
import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController"; import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCodeController";
import { releasePetController } from "@/src/controllers/api/releasePetController"; import { releasePetController } from "@/src/controllers/api/releasePetController";
import { removeFriendGetController, removeFriendPostController } from "@/src/controllers/api/removeFriendController";
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController"; import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController"; import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
import { renamePetController } from "@/src/controllers/api/renamePetController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
@ -125,7 +119,6 @@ import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoC
import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController"; import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController";
import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController"; import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController";
import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController"; import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController";
import { setFriendNoteController } from "@/src/controllers/api/setFriendNoteController";
import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController"; import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController";
import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController"; import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController";
import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController"; import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController";
@ -186,10 +179,8 @@ apiRouter.get("/getGuildContributions.php", getGuildContributionsController);
apiRouter.get("/getGuildDojo.php", getGuildDojoController); apiRouter.get("/getGuildDojo.php", getGuildDojoController);
apiRouter.get("/getGuildLog.php", getGuildLogController); apiRouter.get("/getGuildLog.php", getGuildLogController);
apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController); apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController);
apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17
apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController);
apiRouter.get("/getShip.php", getShipController); apiRouter.get("/getShip.php", getShipController);
apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8
apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/getVendorInfo.php", getVendorInfoController);
apiRouter.get("/hub", hubController); apiRouter.get("/hub", hubController);
apiRouter.get("/hubInstances", hubInstancesController); apiRouter.get("/hubInstances", hubInstancesController);
@ -201,9 +192,7 @@ apiRouter.get("/marketRecommendations.php", marketRecommendationsController);
apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController); apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController);
apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController); apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController);
apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController); apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController);
apiRouter.get("/questControl.php", questControlController);
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
apiRouter.get("/removeFriend.php", removeFriendGetController);
apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); apiRouter.get("/removeFromAlliance.php", removeFromAllianceController);
apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveQuest.php", setActiveQuestController);
apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setActiveShip.php", setActiveShipController);
@ -221,13 +210,10 @@ apiRouter.get("/updateSession.php", updateSessionGetController);
// post // post
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/activateRandomMod.php", activateRandomModController);
apiRouter.post("/addFriend.php", addFriendController);
apiRouter.post("/addFriendImage.php", addFriendImageController); apiRouter.post("/addFriendImage.php", addFriendImageController);
apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
apiRouter.post("/addPendingFriend.php", addPendingFriendController);
apiRouter.post("/addToAlliance.php", addToAllianceController); apiRouter.post("/addToAlliance.php", addToAllianceController);
apiRouter.post("/addToGuild.php", addToGuildController); apiRouter.post("/addToGuild.php", addToGuildController);
apiRouter.post("/adoptPet.php", adoptPetController);
apiRouter.post("/arcaneCommon.php", arcaneCommonController); apiRouter.post("/arcaneCommon.php", arcaneCommonController);
apiRouter.post("/archonFusion.php", archonFusionController); apiRouter.post("/archonFusion.php", archonFusionController);
apiRouter.post("/artifacts.php", artifactsController); apiRouter.post("/artifacts.php", artifactsController);
@ -236,7 +222,6 @@ apiRouter.post("/changeDojoRoot.php", changeDojoRootController);
apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController); apiRouter.post("/claimCompletedRecipe.php", claimCompletedRecipeController);
apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController); apiRouter.post("/clearDialogueHistory.php", clearDialogueHistoryController);
apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController); apiRouter.post("/clearNewEpisodeReward.php", clearNewEpisodeRewardController);
apiRouter.post("/commitStoryModeDecision.php", (_req, res) => { res.end(); }); // U14 (maybe wanna actually unlock the ship features?)
apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController); apiRouter.post("/completeRandomModChallenge.php", completeRandomModChallengeController);
apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationPostController); apiRouter.post("/confirmGuildInvitation.php", confirmGuildInvitationPostController);
apiRouter.post("/contributeGuildClass.php", contributeGuildClassController); apiRouter.post("/contributeGuildClass.php", contributeGuildClassController);
@ -291,13 +276,10 @@ apiRouter.post("/playerSkills.php", playerSkillsController);
apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController); apiRouter.post("/postGuildAdvertisement.php", postGuildAdvertisementController);
apiRouter.post("/projectionManager.php", projectionManagerController); apiRouter.post("/projectionManager.php", projectionManagerController);
apiRouter.post("/purchase.php", purchaseController); apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/questControl.php", questControlController); // U17
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController); apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
apiRouter.post("/releasePet.php", releasePetController); apiRouter.post("/releasePet.php", releasePetController);
apiRouter.post("/removeFriend.php", removeFriendPostController);
apiRouter.post("/removeFromGuild.php", removeFromGuildController); apiRouter.post("/removeFromGuild.php", removeFromGuildController);
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController); apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
apiRouter.post("/renamePet.php", renamePetController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController); apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController); apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
apiRouter.post("/saveDialogue.php", saveDialogueController); apiRouter.post("/saveDialogue.php", saveDialogueController);
@ -310,7 +292,6 @@ apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController);
apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController); apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController);
apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController); apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController);
apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController); apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController);
apiRouter.post("/setFriendNote.php", setFriendNoteController);
apiRouter.post("/setGuildMotd.php", setGuildMotdController); apiRouter.post("/setGuildMotd.php", setGuildMotdController);
apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController); apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController);
apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController); apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);

View File

@ -23,7 +23,6 @@ import { setEvolutionProgressController } from "@/src/controllers/custom/setEvol
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController"; import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController"; import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
import { setBoosterController } from "../controllers/custom/setBoosterController";
const customRouter = express.Router(); const customRouter = express.Router();
@ -47,7 +46,6 @@ customRouter.post("/addXp", addXpController);
customRouter.post("/import", importController); customRouter.post("/import", importController);
customRouter.post("/manageQuests", manageQuestsController); customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController); customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.post("/setBooster", setBoosterController);
customRouter.get("/config", getConfigDataController); customRouter.get("/config", getConfigDataController);
customRouter.post("/config", updateConfigDataController); customRouter.post("/config", updateConfigDataController);

View File

@ -24,8 +24,6 @@ interface IConfig {
infiniteEndo?: boolean; infiniteEndo?: boolean;
infiniteRegalAya?: boolean; infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean; infiniteHelminthMaterials?: boolean;
claimingBlueprintRefundsIngredients?: boolean;
dontSubtractVoidTraces?: boolean;
dontSubtractConsumables?: boolean; dontSubtractConsumables?: boolean;
unlockAllShipFeatures?: boolean; unlockAllShipFeatures?: boolean;
unlockAllShipDecorations?: boolean; unlockAllShipDecorations?: boolean;
@ -44,8 +42,6 @@ interface IConfig {
noVendorPurchaseLimits?: boolean; noVendorPurchaseLimits?: boolean;
noDeathMarks?: boolean; noDeathMarks?: boolean;
noKimCooldowns?: boolean; noKimCooldowns?: boolean;
syndicateMissionsRepeatable?: boolean;
instantFinishRivenChallenge?: boolean;
instantResourceExtractorDrones?: boolean; instantResourceExtractorDrones?: boolean;
noResourceExtractorDronesDamage?: boolean; noResourceExtractorDronesDamage?: boolean;
skipClanKeyCrafting?: boolean; skipClanKeyCrafting?: boolean;
@ -56,18 +52,12 @@ interface IConfig {
noDojoResearchTime?: boolean; noDojoResearchTime?: boolean;
fastClanAscension?: boolean; fastClanAscension?: boolean;
spoofMasteryRank?: number; spoofMasteryRank?: number;
nightwaveStandingMultiplier?: number;
worldState?: { worldState?: {
creditBoost?: boolean; creditBoost?: boolean;
affinityBoost?: boolean; affinityBoost?: boolean;
resourceBoost?: boolean; resourceBoost?: boolean;
starDays?: boolean; starDays?: boolean;
eidolonOverride?: string; lockTime?: number;
vallisOverride?: string;
nightwaveOverride?: string;
};
dev?: {
keepVendorsExpired?: boolean;
}; };
} }

View File

@ -2,7 +2,6 @@ import fs from "fs";
import fsPromises from "fs/promises"; import fsPromises from "fs/promises";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { config, configPath, loadConfig } from "./configService"; import { config, configPath, loadConfig } from "./configService";
import { getWebPorts, startWebServer, stopWebServer } from "./webService";
let amnesia = false; let amnesia = false;
fs.watchFile(configPath, () => { fs.watchFile(configPath, () => {
@ -13,35 +12,17 @@ fs.watchFile(configPath, () => {
try { try {
loadConfig(); loadConfig();
} catch (e) { } catch (e) {
logger.error("FATAL ERROR: Config failed to be reloaded: " + (e as Error).message); logger.error("Failed to reload config.json. Did you delete it?! Execution cannot continue.");
process.exit(1); process.exit(1);
} }
validateConfig(); validateConfig();
const webPorts = getWebPorts();
if (config.httpPort != webPorts.http || config.httpsPort != webPorts.https) {
logger.info(`Restarting web server to apply port changes.`);
void stopWebServer().then(startWebServer);
}
} }
}); });
export const validateConfig = (): void => { export const validateConfig = (): void => {
let modified = false; if (typeof config.administratorNames == "string") {
if (config.administratorNames) { logger.info(`Updating config.json to make administratorNames an array.`);
if (!Array.isArray(config.administratorNames)) { config.administratorNames = [config.administratorNames];
config.administratorNames = [config.administratorNames];
modified = true;
}
for (let i = 0; i != config.administratorNames.length; ++i) {
if (typeof config.administratorNames[i] != "string") {
config.administratorNames[i] = String(config.administratorNames[i]);
modified = true;
}
}
}
if (modified) {
logger.info(`Updating config.json to fix some issues with it.`);
void saveConfig(); void saveConfig();
} }
}; };

View File

@ -1,47 +0,0 @@
import { IFriendInfo } from "../types/friendTypes";
import { getInventory } from "./inventoryService";
import { config } from "./configService";
import { Account } from "../models/loginModel";
import { Types } from "mongoose";
import { Friendship } from "../models/friendModel";
import { fromOid, toMongoDate } from "../helpers/inventoryHelpers";
export const addAccountDataToFriendInfo = async (info: IFriendInfo): Promise<void> => {
const account = (await Account.findById(fromOid(info._id), "DisplayName LastLogin"))!;
info.DisplayName = account.DisplayName;
info.LastLogin = toMongoDate(account.LastLogin);
};
export const addInventoryDataToFriendInfo = async (info: IFriendInfo): Promise<void> => {
const inventory = await getInventory(fromOid(info._id), "PlayerLevel ActiveAvatarImageType");
info.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank;
info.ActiveAvatarImageType = inventory.ActiveAvatarImageType;
};
export const areFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise<boolean> => {
const [aAddedB, bAddedA] = await Promise.all([
Friendship.exists({ owner: a, friend: b }),
Friendship.exists({ owner: b, friend: a })
]);
return Boolean(aAddedB && bAddedA);
};
export const areFriendsOfFriends = async (a: Types.ObjectId | string, b: Types.ObjectId | string): Promise<boolean> => {
const [aInternalFriends, bInternalFriends] = await Promise.all([
Friendship.find({ owner: a }),
Friendship.find({ owner: b })
]);
for (const aInternalFriend of aInternalFriends) {
if (bInternalFriends.find(x => x.friend.equals(aInternalFriend.friend))) {
const c = aInternalFriend.friend;
const [cAcceptedA, cAcceptedB] = await Promise.all([
Friendship.exists({ owner: c, friend: a }),
Friendship.exists({ owner: c, friend: b })
]);
if (cAcceptedA && cAcceptedB) {
return true;
}
}
}
return false;
};

View File

@ -1,5 +1,5 @@
import { Request } from "express"; import { Request } from "express";
import { getAccountIdForRequest, TAccountDocument } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService"; import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
@ -19,11 +19,12 @@ import {
IGuildVault, IGuildVault,
ITechProjectDatabase ITechProjectDatabase
} from "@/src/types/guildTypes"; } from "@/src/types/guildTypes";
import { toMongoDate, toOid, toOid2 } from "@/src/helpers/inventoryHelpers"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources, IDojoBuild, IDojoResearch } from "warframe-public-export-plus"; import { ExportDojoRecipes, ExportResources, IDojoBuild, IDojoResearch } from "warframe-public-export-plus";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { config } from "./configService"; import { config } from "./configService";
import { Account } from "../models/loginModel";
import { getRandomInt } from "./rngService"; import { getRandomInt } from "./rngService";
import { Inbox } from "../models/inboxModel"; import { Inbox } from "../models/inboxModel";
import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes"; import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes";
@ -31,7 +32,6 @@ import { IInventoryChanges } from "../types/purchaseTypes";
import { parallelForeach } from "../utils/async-utils"; import { parallelForeach } from "../utils/async-utils";
import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json"; import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json";
import { createMessage } from "./inboxService"; import { createMessage } from "./inboxService";
import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "./friendService";
export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => { export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -54,10 +54,7 @@ export const getGuildForRequestEx = async (
return guild; return guild;
}; };
export const getGuildClient = async ( export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: string): Promise<IGuildClient> => {
guild: TGuildDatabaseDocument,
account: TAccountDocument
): Promise<IGuildClient> => {
const guildMembers = await GuildMember.find({ guildId: guild._id }); const guildMembers = await GuildMember.find({ guildId: guild._id });
const members: IGuildMemberClient[] = []; const members: IGuildMemberClient[] = [];
@ -65,30 +62,34 @@ export const getGuildClient = async (
const dataFillInPromises: Promise<void>[] = []; const dataFillInPromises: Promise<void>[] = [];
for (const guildMember of guildMembers) { for (const guildMember of guildMembers) {
const member: IGuildMemberClient = { const member: IGuildMemberClient = {
_id: toOid2(guildMember.accountId, account.BuildLabel), _id: toOid(guildMember.accountId),
Rank: guildMember.rank, Rank: guildMember.rank,
Status: guildMember.status, Status: guildMember.status,
Note: guildMember.RequestMsg, Note: guildMember.RequestMsg,
RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined RequestExpiry: guildMember.RequestExpiry ? toMongoDate(guildMember.RequestExpiry) : undefined
}; };
if (guildMember.accountId.equals(account._id)) { if (guildMember.accountId.equals(accountId)) {
missingEntry = false; missingEntry = false;
} else { } else {
dataFillInPromises.push(addAccountDataToFriendInfo(member)); dataFillInPromises.push(
dataFillInPromises.push(addInventoryDataToFriendInfo(member)); (async (): Promise<void> => {
member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName;
})()
);
dataFillInPromises.push(fillInInventoryDataForGuildMember(member));
} }
members.push(member); members.push(member);
} }
if (missingEntry) { if (missingEntry) {
// Handle clans created prior to creation of the GuildMember model. // Handle clans created prior to creation of the GuildMember model.
await GuildMember.insertOne({ await GuildMember.insertOne({
accountId: account._id, accountId: accountId,
guildId: guild._id, guildId: guild._id,
status: 0, status: 0,
rank: 0 rank: 0
}); });
members.push({ members.push({
_id: toOid2(account._id, account.BuildLabel), _id: { $oid: accountId },
Status: 0, Status: 0,
Rank: 0 Rank: 0
}); });
@ -97,7 +98,7 @@ export const getGuildClient = async (
await Promise.all(dataFillInPromises); await Promise.all(dataFillInPromises);
return { return {
_id: toOid2(guild._id, account.BuildLabel), _id: toOid(guild._id),
Name: guild.Name, Name: guild.Name,
MOTD: guild.MOTD, MOTD: guild.MOTD,
LongMOTD: guild.LongMOTD, LongMOTD: guild.LongMOTD,
@ -109,11 +110,11 @@ export const getGuildClient = async (
ActiveDojoColorResearch: guild.ActiveDojoColorResearch, ActiveDojoColorResearch: guild.ActiveDojoColorResearch,
Class: guild.Class, Class: guild.Class,
XP: guild.XP, XP: guild.XP,
IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(account._id)), IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)),
NumContributors: guild.CeremonyContributors?.length ?? 0, NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined, CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
AutoContributeFromVault: guild.AutoContributeFromVault, AutoContributeFromVault: guild.AutoContributeFromVault,
AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined
}; };
}; };
@ -133,11 +134,10 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => {
export const getDojoClient = async ( export const getDojoClient = async (
guild: TGuildDatabaseDocument, guild: TGuildDatabaseDocument,
status: number, status: number,
componentId?: Types.ObjectId | string, componentId?: Types.ObjectId | string
buildLabel?: string
): Promise<IDojoClient> => { ): Promise<IDojoClient> => {
const dojo: IDojoClient = { const dojo: IDojoClient = {
_id: toOid2(guild._id, buildLabel), _id: { $oid: guild._id.toString() },
Name: guild.Name, Name: guild.Name,
Tier: guild.Tier, Tier: guild.Tier,
GuildEmblem: guild.Emblem, GuildEmblem: guild.Emblem,
@ -159,8 +159,8 @@ export const getDojoClient = async (
for (const dojoComponent of guild.DojoComponents) { for (const dojoComponent of guild.DojoComponents) {
if (!componentId || dojoComponent._id.equals(componentId)) { if (!componentId || dojoComponent._id.equals(componentId)) {
const clientComponent: IDojoComponentClient = { const clientComponent: IDojoComponentClient = {
id: toOid2(dojoComponent._id, buildLabel), id: toOid(dojoComponent._id),
SortId: toOid2(dojoComponent.SortId ?? dojoComponent._id, buildLabel), // always providing a SortId so decos don't need repositioning to reparent SortId: toOid(dojoComponent.SortId ?? dojoComponent._id), // always providing a SortId so decos don't need repositioning to reparent
pf: dojoComponent.pf, pf: dojoComponent.pf,
ppf: dojoComponent.ppf, ppf: dojoComponent.ppf,
Name: dojoComponent.Name, Name: dojoComponent.Name,
@ -169,7 +169,7 @@ export const getDojoClient = async (
Settings: dojoComponent.Settings Settings: dojoComponent.Settings
}; };
if (dojoComponent.pi) { if (dojoComponent.pi) {
clientComponent.pi = toOid2(dojoComponent.pi, buildLabel); clientComponent.pi = toOid(dojoComponent.pi);
clientComponent.op = dojoComponent.op!; clientComponent.op = dojoComponent.op!;
clientComponent.pp = dojoComponent.pp!; clientComponent.pp = dojoComponent.pp!;
} }
@ -225,7 +225,7 @@ export const getDojoClient = async (
clientComponent.Decos = []; clientComponent.Decos = [];
for (const deco of dojoComponent.Decos) { for (const deco of dojoComponent.Decos) {
const clientDeco: IDojoDecoClient = { const clientDeco: IDojoDecoClient = {
id: toOid2(deco._id, buildLabel), id: toOid(deco._id),
Type: deco.Type, Type: deco.Type,
Pos: deco.Pos, Pos: deco.Pos,
Rot: deco.Rot, Rot: deco.Rot,
@ -449,7 +449,7 @@ export const addGuildMemberShipDecoContribution = (guildMember: IGuildMemberData
export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => { export const processDojoBuildMaterialsGathered = (guild: TGuildDatabaseDocument, build: IDojoBuild): void => {
if (build.guildXpValue) { if (build.guildXpValue) {
guild.ClaimedXP ??= []; guild.ClaimedXP ??= [];
if (guild.ClaimedXP.indexOf(build.resultType) == -1) { if (!guild.ClaimedXP.find(x => x == build.resultType)) {
guild.ClaimedXP.push(build.resultType); guild.ClaimedXP.push(build.resultType);
guild.XP += build.guildXpValue; guild.XP += build.guildXpValue;
} }
@ -466,6 +466,12 @@ export const setDojoRoomLogFunded = (guild: TGuildDatabaseDocument, component: I
} }
}; };
export const fillInInventoryDataForGuildMember = async (member: IGuildMemberClient): Promise<void> => {
const inventory = await getInventory(member._id.$oid, "PlayerLevel ActiveAvatarImageType");
member.PlayerLevel = config.spoofMasteryRank == -1 ? inventory.PlayerLevel : config.spoofMasteryRank;
member.ActiveAvatarImageType = inventory.ActiveAvatarImageType;
};
export const createUniqueClanName = async (name: string): Promise<string> => { export const createUniqueClanName = async (name: string): Promise<string> => {
const initialDiscriminator = getRandomInt(0, 999); const initialDiscriminator = getRandomInt(0, 999);
let discriminator = initialDiscriminator; let discriminator = initialDiscriminator;

View File

@ -377,6 +377,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db[key] = client[key]; db[key] = client[key];
} }
} }
if (client.EndlessXP !== undefined) {
db.EndlessXP = client.EndlessXP;
}
if (client.SongChallenges !== undefined) { if (client.SongChallenges !== undefined) {
db.SongChallenges = client.SongChallenges; db.SongChallenges = client.SongChallenges;
} }

View File

@ -28,8 +28,7 @@ import {
ITraits, ITraits,
ICalendarProgress, ICalendarProgress,
INemesisWeaponTargetFingerprint, INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint, INemesisPetTargetFingerprint
IDialogueDatabase
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate"; import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes"; import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
@ -44,7 +43,6 @@ import {
import { import {
ExportArcanes, ExportArcanes,
ExportBundles, ExportBundles,
ExportChallenges,
ExportCustoms, ExportCustoms,
ExportDrones, ExportDrones,
ExportEmailItems, ExportEmailItems,
@ -54,6 +52,7 @@ import {
ExportGear, ExportGear,
ExportKeys, ExportKeys,
ExportMisc, ExportMisc,
ExportNightwave,
ExportRailjackWeapons, ExportRailjackWeapons,
ExportRecipes, ExportRecipes,
ExportResources, ExportResources,
@ -71,7 +70,6 @@ import { createShip } from "./shipService";
import { import {
catbrowDetails, catbrowDetails,
fromMongoDate, fromMongoDate,
fromOid,
kubrowDetails, kubrowDetails,
kubrowFurPatternsWeights, kubrowFurPatternsWeights,
kubrowWeights, kubrowWeights,
@ -82,11 +80,9 @@ import { handleBundleAcqusition } from "./purchaseService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json"; import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService"; import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
import { createMessage } from "./inboxService"; import { createMessage } from "./inboxService";
import { getMaxStanding, getMinStanding } from "@/src/helpers/syndicateStandingHelper"; import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { getNightwaveSyndicateTag, getWorldState } from "./worldStateService"; import { getWorldState } from "./worldStateService";
import { generateNemesisProfile, INemesisProfile } from "../helpers/nemesisHelpers"; import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers";
import { TAccountDocument } from "./loginService";
import { unixTimesInMs } from "../constants/timeConstants";
export const createInventory = async ( export const createInventory = async (
accountOwnerId: Types.ObjectId, accountOwnerId: Types.ObjectId,
@ -153,11 +149,6 @@ export const addStartingGear = async (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
startingGear?: TPartialStartingGear startingGear?: TPartialStartingGear
): Promise<IInventoryChanges> => { ): Promise<IInventoryChanges> => {
if (inventory.ReceivedStartingGear) {
throw new Error(`account has already received starting gear`);
}
inventory.ReceivedStartingGear = true;
const { LongGuns, Pistols, Suits, Melee } = startingGear || { const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],
@ -206,6 +197,11 @@ export const addStartingGear = async (
combineInventoryChanges(inventoryChanges, inventoryDelta); combineInventoryChanges(inventoryChanges, inventoryDelta);
} }
if (inventory.ReceivedStartingGear) {
logger.warn(`account already had starting gear but asked for it again?!`);
}
inventory.ReceivedStartingGear = true;
return inventoryChanges; return inventoryChanges;
}; };
@ -427,7 +423,7 @@ export const addItem = async (
changes.push({ changes.push({
ItemType: egg.ItemType, ItemType: egg.ItemType,
ExpirationDate: { $date: { $numberLong: "2000000000000" } }, ExpirationDate: { $date: { $numberLong: "2000000000000" } },
ItemId: toOid(egg._id) // TODO: Pass on buildLabel from purchaseService ItemId: toOid(egg._id)
}); });
} }
return { return {
@ -723,10 +719,6 @@ export const addItem = async (
} }
break; break;
case "Boons":
// Can purchase /Lotus/Upgrades/Boons/DuviriVendorBoonItem from Acrithis, doesn't need to be added to inventory.
return {};
case "Stickers": case "Stickers":
{ {
const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName); const entry = inventory.RawUpgrades.find(x => x.ItemType == typeName);
@ -781,9 +773,7 @@ export const addItem = async (
typeName.substr(1).split("/")[3] == "CatbrowPet" || typeName.substr(1).split("/")[3] == "CatbrowPet" ||
typeName.substr(1).split("/")[3] == "KubrowPet" typeName.substr(1).split("/")[3] == "KubrowPet"
) { ) {
if (typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem") { return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
return addKubrowPet(inventory, typeName, undefined, premiumPurchase);
}
} else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) { } else if (typeName.startsWith("/Lotus/Types/Game/CrewShip/CrewMember/")) {
if (!seed) { if (!seed) {
throw new Error(`Expected crew member to have a seed`); throw new Error(`Expected crew member to have a seed`);
@ -798,12 +788,6 @@ export const addItem = async (
} }
break; break;
} }
case "Items": {
if (typeName.substr(1).split("/")[3] == "Emotes") {
return addCustomization(inventory, typeName);
}
break;
}
case "NeutralCreatures": { case "NeutralCreatures": {
if (inventory.Horses.length != 0) { if (inventory.Horses.length != 0) {
logger.warn("refusing to add Horse because account already has one"); logger.warn("refusing to add Horse because account already has one");
@ -885,14 +869,10 @@ const addSentinel = (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades); const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades);
const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined;
const sentinelIndex = const sentinelIndex =
inventory.Sentinels.push({ inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features, IsNew: true }) -
ItemType: sentinelName, 1;
Configs: configs,
XP: 0,
Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined,
IsNew: inventory.Sentinels.find(x => x.ItemType == sentinelName) ? undefined : true
}) - 1;
inventoryChanges.Sentinels ??= []; inventoryChanges.Sentinels ??= [];
inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>()); inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>());
@ -941,9 +921,6 @@ export const addPowerSuit = async (
}, },
defaultOverwrites defaultOverwrites
); );
if (suit.IsNew) {
suit.IsNew = !inventory.Suits.find(x => x.ItemType == powersuitName);
}
if (!suit.IsNew) { if (!suit.IsNew) {
suit.IsNew = undefined; suit.IsNew = undefined;
} }
@ -978,7 +955,7 @@ export const addMechSuit = async (
UpgradeVer: 101, UpgradeVer: 101,
XP: 0, XP: 0,
Features: features, Features: features,
IsNew: inventory.MechSuits.find(x => x.ItemType == mechsuitName) ? undefined : true IsNew: true
}) - 1; }) - 1;
inventoryChanges.MechSuits ??= []; inventoryChanges.MechSuits ??= [];
inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON<IEquipmentClient>()); inventoryChanges.MechSuits.push(inventory.MechSuits[suitIndex].toJSON<IEquipmentClient>());
@ -1018,7 +995,7 @@ export const addSpaceSuit = (
UpgradeVer: 101, UpgradeVer: 101,
XP: 0, XP: 0,
Features: features, Features: features,
IsNew: inventory.SpaceSuits.find(x => x.ItemType == spacesuitName) ? undefined : true IsNew: true
}) - 1; }) - 1;
inventoryChanges.SpaceSuits ??= []; inventoryChanges.SpaceSuits ??= [];
inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON<IEquipmentClient>()); inventoryChanges.SpaceSuits.push(inventory.SpaceSuits[suitIndex].toJSON<IEquipmentClient>());
@ -1028,13 +1005,12 @@ export const addSpaceSuit = (
export const addKubrowPet = ( export const addKubrowPet = (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
kubrowPetName: string, kubrowPetName: string,
details?: IKubrowPetDetailsDatabase, details: IKubrowPetDetailsDatabase | undefined,
premiumPurchase: boolean = false, premiumPurchase: boolean,
inventoryChanges: IInventoryChanges = {} inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => { ): IInventoryChanges => {
combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)); combineInventoryChanges(inventoryChanges, occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase));
// TODO: When incubating, this should only be given when claiming the recipe.
const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined; const kubrowPet = ExportSentinels[kubrowPetName] as ISentinel | undefined;
const exalted = kubrowPet?.exalted ?? []; const exalted = kubrowPet?.exalted ?? [];
for (const specialItem of exalted) { for (const specialItem of exalted) {
@ -1083,11 +1059,11 @@ export const addKubrowPet = (
details = { details = {
Name: "", Name: "",
IsPuppy: !premiumPurchase, IsPuppy: false,
HasCollar: true, HasCollar: true,
PrintsRemaining: 3, PrintsRemaining: 2,
Status: premiumPurchase ? Status.StatusStasis : Status.StatusIncubating, Status: Status.StatusStasis,
HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start. HatchDate: new Date(Math.trunc(Date.now() / 86400000) * 86400000),
IsMale: !!getRandomInt(0, 1), IsMale: !!getRandomInt(0, 1),
Size: getRandomInt(70, 100) / 100, Size: getRandomInt(70, 100) / 100,
DominantTraits: traits, DominantTraits: traits,
@ -1101,7 +1077,7 @@ export const addKubrowPet = (
Configs: configs, Configs: configs,
XP: 0, XP: 0,
Details: details, Details: details,
IsNew: inventory.KubrowPets.find(x => x.ItemType == kubrowPetName) ? undefined : true IsNew: true
}) - 1; }) - 1;
inventoryChanges.KubrowPets ??= []; inventoryChanges.KubrowPets ??= [];
inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON<IEquipmentClient>()); inventoryChanges.KubrowPets.push(inventory.KubrowPets[kubrowPetIndex].toJSON<IEquipmentClient>());
@ -1129,29 +1105,24 @@ const isCurrencyTracked = (usePremium: boolean): boolean => {
export const updateCurrency = ( export const updateCurrency = (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
price: number, price: number,
usePremium: boolean, usePremium: boolean
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => { ): IInventoryChanges => {
const currencyChanges: IInventoryChanges = {};
if (price != 0 && isCurrencyTracked(usePremium)) { if (price != 0 && isCurrencyTracked(usePremium)) {
if (usePremium) { if (usePremium) {
if (inventory.PremiumCreditsFree > 0) { if (inventory.PremiumCreditsFree > 0) {
const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1; currencyChanges.PremiumCreditsFree = Math.min(price, inventory.PremiumCreditsFree) * -1;
inventoryChanges.PremiumCreditsFree ??= 0; inventory.PremiumCreditsFree += currencyChanges.PremiumCreditsFree;
inventoryChanges.PremiumCreditsFree += premiumCreditsFreeDelta;
inventory.PremiumCreditsFree += premiumCreditsFreeDelta;
} }
inventoryChanges.PremiumCredits ??= 0; currencyChanges.PremiumCredits = -price;
inventoryChanges.PremiumCredits -= price; inventory.PremiumCredits += currencyChanges.PremiumCredits;
inventory.PremiumCredits -= price;
logger.debug(`currency changes `, { PremiumCredits: -price });
} else { } else {
inventoryChanges.RegularCredits ??= 0; currencyChanges.RegularCredits = -price;
inventoryChanges.RegularCredits -= price; inventory.RegularCredits += currencyChanges.RegularCredits;
inventory.RegularCredits -= price;
logger.debug(`currency changes `, { RegularCredits: -price });
} }
logger.debug(`currency changes `, currencyChanges);
} }
return inventoryChanges; return currencyChanges;
}; };
export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => { export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => {
@ -1202,10 +1173,8 @@ export const addStanding = (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
syndicateTag: string, syndicateTag: string,
gainedStanding: number, gainedStanding: number,
affiliationMods: IAffiliationMods[] = [], isMedallion: boolean = false
isMedallion: boolean = false, ): IAffiliationMods => {
propagateAlignments: boolean = true
): void => {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
const syndicateMeta = ExportSyndicates[syndicateTag]; const syndicateMeta = ExportSyndicates[syndicateTag];
@ -1217,10 +1186,6 @@ export const addStanding = (
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0); const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing; if (syndicate.Standing + gainedStanding > max) gainedStanding = max - syndicate.Standing;
if (syndicate.Title == -2 && syndicate.Standing + gainedStanding < -71000) {
gainedStanding = -71000 + syndicate.Standing;
}
if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) { if (!isMedallion || syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) { if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin); gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
@ -1229,27 +1194,10 @@ export const addStanding = (
} }
syndicate.Standing += gainedStanding; syndicate.Standing += gainedStanding;
const affiliationMod: IAffiliationMods = { return {
Tag: syndicateTag, Tag: syndicateTag,
Standing: gainedStanding Standing: gainedStanding
}; };
affiliationMods.push(affiliationMod);
if (syndicateMeta.alignments) {
if (propagateAlignments) {
for (const [tag, factor] of Object.entries(syndicateMeta.alignments)) {
addStanding(inventory, tag, gainedStanding * factor, affiliationMods, isMedallion, false);
}
} else {
while (syndicate.Standing < getMinStanding(syndicateMeta, syndicate.Title ?? 0)) {
syndicate.Title ??= 0;
syndicate.Title -= 1;
affiliationMod.Title ??= 0;
affiliationMod.Title -= 1;
logger.debug(`${syndicateTag} is decreasing to title ${syndicate.Title} after applying alignment`);
}
}
}
}; };
// TODO: AffiliationMods support (Nightwave). // TODO: AffiliationMods support (Nightwave).
@ -1309,9 +1257,6 @@ export const addEquipment = (
}, },
defaultOverwrites defaultOverwrites
); );
if (equipment.IsNew) {
equipment.IsNew = !inventory[category].find(x => x.ItemType == type);
}
if (!equipment.IsNew) { if (!equipment.IsNew) {
equipment.IsNew = undefined; equipment.IsNew = undefined;
} }
@ -1546,10 +1491,9 @@ export const applyClientEquipmentUpdates = (
const category = inventory[categoryName]; const category = inventory[categoryName];
gearArray.forEach(({ ItemId, XP, InfestationDate }) => { gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
const item = category.id(fromOid(ItemId)); const item = category.id(ItemId.$oid);
if (!item) { if (!item) {
logger.warn(`Skipping unknown ${categoryName} item: id ${fromOid(ItemId)} not found`); throw new Error(`No item with id ${ItemId.$oid} in ${categoryName}`);
return;
} }
if (XP) { if (XP) {
@ -1625,17 +1569,12 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray:
if (MiscItems[itemIndex].ItemCount == 0) { if (MiscItems[itemIndex].ItemCount == 0) {
MiscItems.splice(itemIndex, 1); MiscItems.splice(itemIndex, 1);
} else if (MiscItems[itemIndex].ItemCount <= 0) { } else if (MiscItems[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.MiscItems has a negative count for ${ItemType}`); logger.warn(`account now owns a negative amount of ${ItemType}`);
} }
}); });
}; };
const applyArrayChanges = ( const applyArrayChanges = (arr: ITypeCount[], changes: ITypeCount[]): void => {
inventory: TInventoryDatabaseDocument,
key: "ShipDecorations" | "Consumables" | "CrewShipRawSalvage" | "CrewShipAmmo" | "Recipes" | "LevelKeys",
changes: ITypeCount[]
): void => {
const arr: ITypeCount[] = inventory[key];
for (const change of changes) { for (const change of changes) {
if (change.ItemCount != 0) { if (change.ItemCount != 0) {
let itemIndex = arr.findIndex(x => x.ItemType === change.ItemType); let itemIndex = arr.findIndex(x => x.ItemType === change.ItemType);
@ -1647,34 +1586,34 @@ const applyArrayChanges = (
if (arr[itemIndex].ItemCount == 0) { if (arr[itemIndex].ItemCount == 0) {
arr.splice(itemIndex, 1); arr.splice(itemIndex, 1);
} else if (arr[itemIndex].ItemCount <= 0) { } else if (arr[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.${key} has a negative count for ${change.ItemType}`); logger.warn(`account now owns a negative amount of ${change.ItemType}`);
} }
} }
} }
}; };
export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { export const addShipDecorations = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory, "ShipDecorations", itemsArray); applyArrayChanges(inventory.ShipDecorations, itemsArray);
}; };
export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory, "Consumables", itemsArray); applyArrayChanges(inventory.Consumables, itemsArray);
}; };
export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { export const addCrewShipRawSalvage = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory, "CrewShipRawSalvage", itemsArray); applyArrayChanges(inventory.CrewShipRawSalvage, itemsArray);
}; };
export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory, "CrewShipAmmo", itemsArray); applyArrayChanges(inventory.CrewShipAmmo, itemsArray);
}; };
export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory, "Recipes", itemsArray); applyArrayChanges(inventory.Recipes, itemsArray);
}; };
export const addLevelKeys = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => { export const addLevelKeys = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[]): void => {
applyArrayChanges(inventory, "LevelKeys", itemsArray); applyArrayChanges(inventory.LevelKeys, itemsArray);
}; };
export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => { export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawUpgrade[]): void => {
@ -1694,7 +1633,7 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU
if (RawUpgrades[itemIndex].ItemCount == 0) { if (RawUpgrades[itemIndex].ItemCount == 0) {
RawUpgrades.splice(itemIndex, 1); RawUpgrades.splice(itemIndex, 1);
} else if (RawUpgrades[itemIndex].ItemCount <= 0) { } else if (RawUpgrades[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`); logger.warn(`account now owns a negative amount of ${ItemType}`);
} }
}); });
}; };
@ -1709,7 +1648,7 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA
if (FusionTreasures[itemIndex].ItemCount == 0) { if (FusionTreasures[itemIndex].ItemCount == 0) {
FusionTreasures.splice(itemIndex, 1); FusionTreasures.splice(itemIndex, 1);
} else if (FusionTreasures[itemIndex].ItemCount <= 0) { } else if (FusionTreasures[itemIndex].ItemCount <= 0) {
logger.warn(`inventory.FusionTreasures has a negative count for ${ItemType}`); logger.warn(`account now owns a negative amount of ${ItemType}`);
} }
} else { } else {
FusionTreasures.push({ ItemCount, ItemType, Sockets }); FusionTreasures.push({ ItemCount, ItemType, Sockets });
@ -1755,7 +1694,6 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
}; };
export const addChallenges = ( export const addChallenges = (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
ChallengeProgress: IChallengeProgress[], ChallengeProgress: IChallengeProgress[],
SeasonChallengeCompletions: ISeasonChallenge[] | undefined SeasonChallengeCompletions: ISeasonChallenge[] | undefined
@ -1778,33 +1716,26 @@ export const addChallenges = (
continue; continue;
} }
const meta = ExportChallenges[challenge.challenge]; const meta = ExportNightwave.challenges[challenge.challenge];
const nightwaveSyndicateTag = getNightwaveSyndicateTag(account.BuildLabel); logger.debug("Completed challenge", meta);
logger.debug("Completed season challenge", {
uniqueName: challenge.challenge,
syndicateTag: nightwaveSyndicateTag,
...meta
});
if (nightwaveSyndicateTag) {
let affiliation = inventory.Affiliations.find(x => x.Tag == nightwaveSyndicateTag);
if (!affiliation) {
affiliation =
inventory.Affiliations[
inventory.Affiliations.push({
Tag: nightwaveSyndicateTag,
Standing: 0
}) - 1
];
}
const standingToAdd = meta.standing! * (config.nightwaveStandingMultiplier ?? 1); let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
affiliation.Standing += standingToAdd; if (!affiliation) {
if (affiliationMods.length == 0) { affiliation =
affiliationMods.push({ Tag: nightwaveSyndicateTag }); inventory.Affiliations[
} inventory.Affiliations.push({
affiliationMods[0].Standing ??= 0; Tag: ExportNightwave.affiliationTag,
affiliationMods[0].Standing += standingToAdd; Standing: 0
}) - 1
];
} }
affiliation.Standing += meta.standing;
if (affiliationMods.length == 0) {
affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
}
affiliationMods[0].Standing ??= 0;
affiliationMods[0].Standing += meta.standing;
} }
} }
return affiliationMods; return affiliationMods;
@ -1954,29 +1885,6 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
} }
}; };
export 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;
};
export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => { export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => {
const currentSeason = getWorldState().KnownCalendarSeasons[0]; const currentSeason = getWorldState().KnownCalendarSeasons[0];
@ -2016,7 +1924,8 @@ export const giveNemesisWeaponRecipe = (
weaponType: string, weaponType: string,
nemesisName: string = "AGOR ROK", nemesisName: string = "AGOR ROK",
weaponLoc?: string, weaponLoc?: string,
profile: INemesisProfile = generateNemesisProfile() KillingSuit: string = "/Lotus/Powersuits/Ember/Ember",
fp: bigint = generateRewardSeed()
): void => { ): void => {
if (!weaponLoc) { if (!weaponLoc) {
weaponLoc = ExportWeapons[weaponType].name; weaponLoc = ExportWeapons[weaponType].name;
@ -2037,8 +1946,8 @@ export const giveNemesisWeaponRecipe = (
compat: weaponType, compat: weaponType,
buffs: [ buffs: [
{ {
Tag: profile.innateDamageTag, Tag: getInnateDamageTag(KillingSuit),
Value: profile.innateDamageValue Value: getInnateDamageValue(fp)
} }
] ]
}, },
@ -2047,15 +1956,27 @@ export const giveNemesisWeaponRecipe = (
}); });
}; };
export const giveNemesisPetRecipe = ( export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => {
inventory: TInventoryDatabaseDocument, const head = getRandomElement([
nemesisName: string = "AGOR ROK", "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
profile: INemesisProfile = generateNemesisProfile() "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
): void => { "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
const head = profile.petHead!; ])!;
const body = profile.petBody!; const body = getRandomElement([
const legs = profile.petLegs!; "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
const tail = profile.petTail!; "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
const legs = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
const tail = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0]; const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0];
addRecipes(inventory, [ addRecipes(inventory, [
{ {
@ -2072,7 +1993,3 @@ export const giveNemesisPetRecipe = (
} satisfies INemesisPetTargetFingerprint) } satisfies INemesisPetTargetFingerprint)
}); });
}; };
export const getEffectiveAvatarImageType = (inventory: TInventoryDatabaseDocument): string => {
return inventory.ActiveAvatarImageType ?? "/Lotus/Types/StoreItems/AvatarImages/AvatarImageDefault";
};

View File

@ -1,4 +1,5 @@
import { IKeyChainRequest } from "@/src/types/requestTypes"; import { IKeyChainRequest } from "@/src/types/requestTypes";
import { getIndexAfter } from "@/src/helpers/stringHelpers";
import { import {
dict_de, dict_de,
dict_en, dict_en,
@ -52,32 +53,20 @@ export const getRecipeByResult = (resultType: string): IRecipe | undefined => {
return Object.values(ExportRecipes).find(x => x.resultType == resultType); return Object.values(ExportRecipes).find(x => x.resultType == resultType);
}; };
export const getItemCategoryByUniqueName = (uniqueName: string): string | undefined => { export const getItemCategoryByUniqueName = (uniqueName: string): string => {
if (uniqueName in ExportCustoms) { //Lotus/Types/Items/MiscItems/PolymerBundle
return ExportCustoms[uniqueName].productCategory;
let splitWord = "Items/";
if (!uniqueName.includes("/Items/")) {
splitWord = "/Types/";
} }
if (uniqueName in ExportDrones) {
return "Drones"; const index = getIndexAfter(uniqueName, splitWord);
if (index === -1) {
throw new Error(`error parsing item category ${uniqueName}`);
} }
if (uniqueName in ExportKeys) { const category = uniqueName.substring(index).split("/")[0];
return "LevelKeys"; return category;
}
if (uniqueName in ExportGear) {
return "Consumables";
}
if (uniqueName in ExportResources) {
return ExportResources[uniqueName].productCategory;
}
if (uniqueName in ExportSentinels) {
return ExportSentinels[uniqueName].productCategory;
}
if (uniqueName in ExportWarframes) {
return ExportWarframes[uniqueName].productCategory;
}
if (uniqueName in ExportWeapons) {
return ExportWeapons[uniqueName].productCategory;
}
return undefined;
}; };
export const getItemName = (uniqueName: string): string | undefined => { export const getItemName = (uniqueName: string): string | undefined => {
@ -233,7 +222,7 @@ export const isStoreItem = (type: string): boolean => {
}; };
export const toStoreItem = (type: string): string => { export const toStoreItem = (type: string): string => {
if (type.startsWith("/Lotus/Types/Boosters/")) { if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type); const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
if (boosterEntry) { if (boosterEntry) {
return boosterEntry[0]; return boosterEntry[0];

View File

@ -1,17 +1,11 @@
import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json"; import randomRewards from "@/static/fixed_responses/loginRewards/randomRewards.json";
import { IInventoryChanges } from "../types/purchaseTypes"; import { IInventoryChanges } from "../types/purchaseTypes";
import { TAccountDocument } from "./loginService"; import { TAccountDocument } from "./loginService";
import { mixSeeds, SRng } from "./rngService"; import { CRng, mixSeeds } from "./rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { addBooster, updateCurrency } from "./inventoryService"; import { addBooster, updateCurrency } from "./inventoryService";
import { handleStoreItemAcquisition } from "./purchaseService"; import { handleStoreItemAcquisition } from "./purchaseService";
import { import { ExportBoosters, ExportRecipes, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
ExportBoosterPacks,
ExportBoosters,
ExportRecipes,
ExportWarframes,
ExportWeapons
} from "warframe-public-export-plus";
import { toStoreItem } from "./itemDataService"; import { toStoreItem } from "./itemDataService";
export interface ILoginRewardsReponse { export interface ILoginRewardsReponse {
@ -55,8 +49,8 @@ const scaleAmount = (day: number, amount: number, scalingMultiplier: number): nu
// Always produces the same result for the same account _id & LoginDays pair. // Always produces the same result for the same account _id & LoginDays pair.
export const isLoginRewardAChoice = (account: TAccountDocument): boolean => { export const isLoginRewardAChoice = (account: TAccountDocument): boolean => {
const accountSeed = parseInt(account._id.toString().substring(16), 16); const accountSeed = parseInt(account._id.toString().substring(16), 16);
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays)); const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
return rng.randomFloat() < 0.25; return rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
}; };
// Always produces the same result for the same account _id & LoginDays pair. // Always produces the same result for the same account _id & LoginDays pair.
@ -65,8 +59,8 @@ export const getRandomLoginRewards = (
inventory: TInventoryDatabaseDocument inventory: TInventoryDatabaseDocument
): ILoginReward[] => { ): ILoginReward[] => {
const accountSeed = parseInt(account._id.toString().substring(16), 16); const accountSeed = parseInt(account._id.toString().substring(16), 16);
const rng = new SRng(mixSeeds(accountSeed, account.LoginDays)); const rng = new CRng(mixSeeds(accountSeed, account.LoginDays));
const pick_a_door = rng.randomFloat() < 0.25; const pick_a_door = rng.random() < 0.25; // Using 25% as an approximate chance for pick-a-doors. More conclusive data analysis is needed.
const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)]; const rewards = [getRandomLoginReward(rng, account.LoginDays, inventory)];
if (pick_a_door) { if (pick_a_door) {
do { do {
@ -79,10 +73,9 @@ export const getRandomLoginRewards = (
return rewards; return rewards;
}; };
const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => { const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatabaseDocument): ILoginReward => {
const reward = rng.randomReward(randomRewards)!; const reward = rng.randomReward(randomRewards)!;
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!; //const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
let storeItemType: string = reward.StoreItemType;
if (reward.RewardType == "RT_RANDOM_RECIPE") { if (reward.RewardType == "RT_RANDOM_RECIPE") {
const masteredItems = new Set(); const masteredItems = new Set();
for (const entry of inventory.XPInfo) { for (const entry of inventory.XPInfo) {
@ -109,12 +102,7 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab
// This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward. // This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
return getRandomLoginReward(rng, day, inventory); return getRandomLoginReward(rng, day, inventory);
} }
storeItemType = toStoreItem(rng.randomElement(eligibleRecipes)!); reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
} else if (reward.StoreItemType == "/Lotus/StoreItems/Types/BoosterPacks/LoginRewardRandomProjection") {
storeItemType = toStoreItem(
rng.randomElement(ExportBoosterPacks["/Lotus/Types/BoosterPacks/LoginRewardRandomProjection"].components)!
.Item
);
} }
return { return {
//_id: toOid(new Types.ObjectId()), //_id: toOid(new Types.ObjectId()),
@ -122,7 +110,7 @@ const getRandomLoginReward = (rng: SRng, day: number, inventory: TInventoryDatab
//CouponType: "CPT_PLATINUM", //CouponType: "CPT_PLATINUM",
Icon: reward.Icon ?? "", Icon: reward.Icon ?? "",
//ItemType: "", //ItemType: "",
StoreItemType: storeItemType, StoreItemType: reward.StoreItemType,
//ProductCategory: "Pistols", //ProductCategory: "Pistols",
Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)), Amount: reward.Duration ? 1 : Math.round(scaleAmount(day, reward.Amount, reward.ScalingMultiplier)),
ScalingMultiplier: reward.ScalingMultiplier, ScalingMultiplier: reward.ScalingMultiplier,

View File

@ -74,7 +74,7 @@ export const getAccountForRequest = async (req: Request): Promise<TAccountDocume
throw new Error("Request is missing nonce parameter"); throw new Error("Request is missing nonce parameter");
} }
const account = await Account.findById(req.query.accountId as string); const account = await Account.findById(req.query.accountId);
if (!account || account.Nonce != nonce) { if (!account || account.Nonce != nonce) {
throw new Error("Invalid accountId-nonce pair"); throw new Error("Invalid accountId-nonce pair");
} }
@ -90,7 +90,7 @@ export const getAccountIdForRequest = async (req: Request): Promise<string> => {
}; };
export const isAdministrator = (account: TAccountDocument): boolean => { export const isAdministrator = (account: TAccountDocument): boolean => {
return config.administratorNames?.indexOf(account.DisplayName) != -1; return !!config.administratorNames?.find(x => x == account.DisplayName);
}; };
const platform_magics = [753, 639, 247, 37, 60]; const platform_magics = [753, 639, 247, 37, 60];

File diff suppressed because it is too large Load Diff

View File

@ -9,7 +9,7 @@ import {
updateSlots updateSlots
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { getRandomWeightedRewardUc } from "@/src/services/rngService"; import { getRandomWeightedRewardUc } from "@/src/services/rngService";
import { applyStandingToVendorManifest, getVendorManifestByOid } from "@/src/services/serversideVendorsService"; import { getVendorManifestByOid } from "@/src/services/serversideVendorsService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes"; import { IPurchaseRequest, IPurchaseResponse, SlotPurchase, IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
@ -53,9 +53,8 @@ export const handlePurchase = async (
const prePurchaseInventoryChanges: IInventoryChanges = {}; const prePurchaseInventoryChanges: IInventoryChanges = {};
let seed: bigint | undefined; let seed: bigint | undefined;
if (purchaseRequest.PurchaseParams.Source == 7) { if (purchaseRequest.PurchaseParams.Source == 7) {
let manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!); const manifest = getVendorManifestByOid(purchaseRequest.PurchaseParams.SourceId!);
if (manifest) { if (manifest) {
manifest = applyStandingToVendorManifest(inventory, manifest);
let ItemId: string | undefined; let ItemId: string | undefined;
if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) { if (purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) {
ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string }) ItemId = (JSON.parse(purchaseRequest.PurchaseParams.ExtraPurchaseInfoJson) as { ItemId: string })
@ -93,7 +92,7 @@ export const handlePurchase = async (
if (!config.noVendorPurchaseLimits && ItemId) { if (!config.noVendorPurchaseLimits && ItemId) {
inventory.RecentVendorPurchases ??= []; inventory.RecentVendorPurchases ??= [];
let vendorPurchases = inventory.RecentVendorPurchases.find( let vendorPurchases = inventory.RecentVendorPurchases.find(
x => x.VendorType == manifest!.VendorInfo.TypeName x => x.VendorType == manifest.VendorInfo.TypeName
); );
if (!vendorPurchases) { if (!vendorPurchases) {
vendorPurchases = vendorPurchases =

View File

@ -86,12 +86,54 @@ export const mixSeeds = (seed1: number, seed2: number): number => {
return seed >>> 0; return seed >>> 0;
}; };
// Seeded RNG with identical results to the game client. Based on work by Donald Knuth. // Seeded RNG for internal usage. Based on recommendations in the ISO C standards.
export class CRng {
state: number;
constructor(seed: number = 1) {
this.state = seed;
}
random(): number {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
return (this.state & 0x3fffffff) / 0x3fffffff;
}
randomInt(min: number, max: number): number {
const diff = max - min;
if (diff != 0) {
if (diff < 0) {
throw new Error(`max must be greater than min`);
}
if (diff > 0x3fffffff) {
throw new Error(`insufficient entropy`);
}
min += Math.floor(this.random() * (diff + 1));
}
return min;
}
randomElement<T>(arr: readonly T[]): T | undefined {
return arr[Math.floor(this.random() * arr.length)];
}
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.random());
}
churnSeed(its: number): void {
while (its--) {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
}
}
}
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
export class SRng { export class SRng {
state: bigint; state: bigint;
constructor(seed: bigint | number) { constructor(seed: bigint) {
this.state = BigInt(seed); this.state = seed;
} }
randomInt(min: number, max: number): number { randomInt(min: number, max: number): number {
@ -115,19 +157,4 @@ export class SRng {
randomReward<T extends { probability: number }>(pool: T[]): T | undefined { randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.randomFloat()); return getRewardAtPercentage(pool, this.randomFloat());
} }
churnSeed(its: number): void {
while (its--) {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
}
}
shuffleArray<T>(arr: T[]): void {
for (let lastIdx = arr.length - 1; lastIdx >= 1; --lastIdx) {
const swapIdx = this.randomInt(0, lastIdx);
const tmp = arr[swapIdx];
arr[swapIdx] = arr[lastIdx];
arr[lastIdx] = tmp;
}
}
} }

View File

@ -1,12 +1,9 @@
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { isDev } from "@/src/helpers/pathHelper";
import { catBreadHash } from "@/src/helpers/stringHelpers"; import { catBreadHash } from "@/src/helpers/stringHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { CRng, mixSeeds } from "@/src/services/rngService";
import { mixSeeds, SRng } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes"; import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes"; import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { logger } from "@/src/utils/logger"; import { ExportVendors, IRange } from "warframe-public-export-plus";
import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json"; import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json"; import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
@ -17,14 +14,19 @@ import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_response
import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json"; import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json";
import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json"; import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json";
import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json"; import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json";
import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json";
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json"; import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json"; import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json"; import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json"; import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json"; import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json"; import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json"; import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json"; import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json"; import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json"; import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json"; import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
@ -39,14 +41,19 @@ const rawVendorManifests: IVendorManifest[] = [
DeimosHivemindCommisionsManifestWeaponsmith, DeimosHivemindCommisionsManifestWeaponsmith,
DeimosHivemindTokenVendorManifest, DeimosHivemindTokenVendorManifest,
DeimosPetVendorManifest, DeimosPetVendorManifest,
DeimosProspectorVendorManifest,
DuviriAcrithisVendorManifest, DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest, EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest, EntratiLabsEntratiLabVendorManifest,
HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest, HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest, MaskSalesmanManifest,
Nova1999ConquestShopManifest, Nova1999ConquestShopManifest,
OstronPetVendorManifest, OstronPetVendorManifest,
OstronProspectorVendorManifest,
RadioLegionIntermission12VendorManifest,
SolarisDebtTokenVendorRepossessionsManifest, SolarisDebtTokenVendorRepossessionsManifest,
SolarisProspectorVendorManifest,
Temple1999VendorManifest, Temple1999VendorManifest,
TeshinHardModeVendorManifest, // uses preprocessing TeshinHardModeVendorManifest, // uses preprocessing
ZarimanCommisionsManifestArchimedean ZarimanCommisionsManifestArchimedean
@ -74,32 +81,23 @@ const generatableVendors: IGeneratableVendorInfo[] = [
WeaponUpgradeValueAttenuationExponent: 2.25, WeaponUpgradeValueAttenuationExponent: 2.25,
cycleOffset: 1744934400_000, cycleOffset: 1744934400_000,
cycleDuration: 4 * unixTimesInMs.day cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "61ba123467e5d37975aeeb03" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
RandomSeedType: "VRST_FLAVOUR_TEXT",
cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly.
} }
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest"
// }
]; ];
const getVendorOid = (typeName: string): string => { const getVendorOid = (typeName: string): string => {
return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0"); return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0");
}; };
// https://stackoverflow.com/a/17445304
const gcd = (a: number, b: number): number => {
return b ? gcd(b, a % b) : a;
};
const getCycleDuration = (manifest: IVendor): number => {
let dur = 0;
for (const item of manifest.items) {
if (typeof item.durationHours != "number") {
dur = 1;
break;
}
if (dur != item.durationHours) {
dur = gcd(dur, item.durationHours);
}
}
return dur * unixTimesInMs.hour;
};
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => { export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
for (const vendorManifest of rawVendorManifests) { for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) { if (vendorManifest.VendorInfo.TypeName == typeName) {
@ -112,12 +110,11 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest |
} }
} }
if (typeName in ExportVendors) { if (typeName in ExportVendors) {
const manifest = ExportVendors[typeName];
return generateVendorManifest({ return generateVendorManifest({
_id: { $oid: getVendorOid(typeName) }, _id: { $oid: getVendorOid(typeName) },
TypeName: typeName, TypeName: typeName,
RandomSeedType: manifest.randomSeedType, RandomSeedType: ExportVendors[typeName].randomSeedType,
cycleDuration: getCycleDuration(manifest) cycleDuration: unixTimesInMs.hour
}); });
} }
return undefined; return undefined;
@ -141,50 +138,13 @@ export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined
_id: { $oid: typeNameOid }, _id: { $oid: typeNameOid },
TypeName: typeName, TypeName: typeName,
RandomSeedType: manifest.randomSeedType, RandomSeedType: manifest.randomSeedType,
cycleDuration: getCycleDuration(manifest) cycleDuration: unixTimesInMs.hour
}); });
} }
} }
return undefined; return undefined;
}; };
export const applyStandingToVendorManifest = (
inventory: TInventoryDatabaseDocument,
vendorManifest: IVendorManifest
): IVendorManifest => {
return {
VendorInfo: {
...vendorManifest.VendorInfo,
ItemManifest: [...vendorManifest.VendorInfo.ItemManifest].map(offer => {
if (offer.Affiliation && offer.ReductionPerPositiveRank && offer.IncreasePerNegativeRank) {
const title: number = inventory.Affiliations.find(x => x.Tag == offer.Affiliation)?.Title ?? 0;
const factor =
1 + (title < 0 ? offer.IncreasePerNegativeRank : offer.ReductionPerPositiveRank) * title * -1;
//console.log(offer.Affiliation, title, factor);
if (factor) {
offer = { ...offer };
if (offer.RegularPrice) {
offer.RegularPriceBeforeDiscount = offer.RegularPrice;
offer.RegularPrice = [
Math.trunc(offer.RegularPriceBeforeDiscount[0] * factor),
Math.trunc(offer.RegularPriceBeforeDiscount[1] * factor)
];
}
if (offer.ItemPrices) {
offer.ItemPricesBeforeDiscount = offer.ItemPrices;
offer.ItemPrices = [];
for (const item of offer.ItemPricesBeforeDiscount) {
offer.ItemPrices.push({ ...item, ItemCount: Math.trunc(item.ItemCount * factor) });
}
}
}
}
return offer;
})
}
};
};
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => { const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) { if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
const manifest = structuredClone(originalManifest); const manifest = structuredClone(originalManifest);
@ -216,49 +176,24 @@ const toRange = (value: IRange | number): IRange => {
return value; return value;
}; };
const getCycleDurationRange = (manifest: IVendor): IRange | undefined => { const vendorInfoCache: Record<string, IVendorInfo> = {};
const res: IRange = { minValue: Number.MAX_SAFE_INTEGER, maxValue: 0 };
for (const offer of manifest.items) {
if (offer.durationHours) {
const range = toRange(offer.durationHours);
if (res.minValue > range.minValue) {
res.minValue = range.minValue;
}
if (res.maxValue < range.maxValue) {
res.maxValue = range.maxValue;
}
}
}
return res.maxValue != 0 ? res : undefined;
};
const vendorManifestCache: Record<string, IVendorManifest> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => { const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
if (!(vendorInfo.TypeName in vendorManifestCache)) { if (!(vendorInfo.TypeName in vendorInfoCache)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo; const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
vendorManifestCache[vendorInfo.TypeName] = { vendorInfoCache[vendorInfo.TypeName] = {
VendorInfo: { ...clientVendorInfo,
...clientVendorInfo, ItemManifest: [],
ItemManifest: [], Expiry: { $date: { $numberLong: "0" } }
Expiry: { $date: { $numberLong: "0" } }
}
}; };
} }
const cacheEntry = vendorManifestCache[vendorInfo.TypeName]; const processed = vendorInfoCache[vendorInfo.TypeName];
const info = cacheEntry.VendorInfo; if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) {
const manifest = ExportVendors[vendorInfo.TypeName];
const cycleDurationRange = getCycleDurationRange(manifest);
let now = Date.now();
if (cycleDurationRange && cycleDurationRange.minValue != cycleDurationRange.maxValue) {
now -= (cycleDurationRange.maxValue - 1) * unixTimesInMs.hour;
}
while (Date.now() >= parseInt(info.Expiry.$date.$numberLong)) {
// Remove expired offers // Remove expired offers
for (let i = 0; i != info.ItemManifest.length; ) { for (let i = 0; i != processed.ItemManifest.length; ) {
if (now >= parseInt(info.ItemManifest[i].Expiry.$date.$numberLong)) { if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) {
info.ItemManifest.splice(i, 1); processed.ItemManifest.splice(i, 1);
} else { } else {
++i; ++i;
} }
@ -268,63 +203,36 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16); const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000; const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
const cycleDuration = vendorInfo.cycleDuration; const cycleDuration = vendorInfo.cycleDuration;
const cycleIndex = Math.trunc((now - cycleOffset) / cycleDuration); const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
const rng = new SRng(mixSeeds(vendorSeed, cycleIndex)); const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
const offersToAdd: IVendorOffer[] = []; const manifest = ExportVendors[vendorInfo.TypeName];
if (!manifest.isOneBinPerCycle) { const offersToAdd = [];
const remainingItemCapacity: Record<string, number> = {}; if (manifest.numItems && !manifest.isOneBinPerCycle) {
for (const item of manifest.items) { const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
remainingItemCapacity[item.storeItem] = 1 + item.duplicates; while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) {
} // TODO: Consider per-bin item limits
for (const offer of info.ItemManifest) { // TODO: Consider item probability weightings
remainingItemCapacity[offer.StoreItem] -= 1; offersToAdd.push(rng.randomElement(manifest.items)!);
}
if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) {
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
// TODO: Consider per-bin item limits
// TODO: Consider item probability weightings
const item = rng.randomElement(manifest.items)!;
if (remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
}
}
} else {
for (const item of manifest.items) {
if (!item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
}
}
for (const e of Object.entries(remainingItemCapacity)) {
const item = manifest.items.find(x => x.storeItem == e[0])!;
if (!item.alwaysOffered) {
while (e[1] != 0) {
e[1] -= 1;
offersToAdd.push(item);
}
}
}
for (const item of manifest.items) {
if (item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
remainingItemCapacity[item.storeItem] -= 1;
offersToAdd.push(item);
}
}
offersToAdd.reverse();
} }
} else { } else {
const binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now. let binThisCycle;
if (manifest.isOneBinPerCycle) {
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
}
for (const rawItem of manifest.items) { for (const rawItem of manifest.items) {
if (rawItem.bin == binThisCycle) { if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
offersToAdd.push(rawItem); offersToAdd.push(rawItem);
} }
} }
// For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception.
if (!manifest.isOneBinPerCycle) {
offersToAdd.reverse();
}
} }
const cycleStart = cycleOffset + cycleIndex * cycleDuration; const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) { for (const rawItem of offersToAdd) {
const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration); const durationHoursRange = toRange(rawItem.durationHours);
const expiry = const expiry =
cycleStart + cycleStart +
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour; rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
@ -332,14 +240,15 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
StoreItem: rawItem.storeItem, StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })), ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin, Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: rawItem.quantity, QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: expiry.toString() } }, Expiry: { $date: { $numberLong: expiry.toString() } },
AllowMultipurchase: false, AllowMultipurchase: false,
Id: { Id: {
$oid: $oid:
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
vendorInfo._id.$oid.substring(8, 16) + vendorInfo._id.$oid.substring(8, 16) +
rng.randomInt(0, 0xffff_ffff).toString(16).padStart(8, "0") rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
} }
}; };
if (rawItem.numRandomItemPrices) { if (rawItem.numRandomItemPrices) {
@ -374,54 +283,26 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
item.PremiumPrice = [value, value]; item.PremiumPrice = [value, value];
} }
if (vendorInfo.RandomSeedType) { if (vendorInfo.RandomSeedType) {
item.LocTagRandSeed = rng.randomInt(0, 0xffff_ffff); item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
if (vendorInfo.RandomSeedType == "VRST_WEAPON") { if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
const highDword = rng.randomInt(0, 0xffff_ffff); const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn); item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
} }
} }
info.ItemManifest.push(item); processed.ItemManifest.push(item);
} }
// Update vendor expiry // Update vendor expiry
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER; let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
for (const offer of info.ItemManifest) { for (const offer of processed.ItemManifest) {
const offerExpiry = parseInt(offer.Expiry.$date.$numberLong); const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
if (soonestOfferExpiry > offerExpiry) { if (soonestOfferExpiry > offerExpiry) {
soonestOfferExpiry = offerExpiry; soonestOfferExpiry = offerExpiry;
} }
} }
info.Expiry.$date.$numberLong = soonestOfferExpiry.toString(); processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
now += unixTimesInMs.hour;
} }
return cacheEntry; return {
VendorInfo: processed
};
}; };
if (isDev) {
const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")!
.VendorInfo.ItemManifest;
if (
ads.length != 5 ||
ads[0].Bin != "BIN_4" ||
ads[1].Bin != "BIN_3" ||
ads[2].Bin != "BIN_2" ||
ads[3].Bin != "BIN_1" ||
ads[4].Bin != "BIN_0"
) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest`);
}
const pall = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest")!
.VendorInfo.ItemManifest;
if (
pall.length != 5 ||
pall[0].StoreItem != "/Lotus/StoreItems/Types/Items/ShipDecos/HarrowQuestKeyOrnament" ||
pall[1].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack" ||
pall[2].StoreItem != "/Lotus/StoreItems/Types/StoreItems/CreditBundles/150000Credits" ||
pall[3].StoreItem != "/Lotus/StoreItems/Types/Items/MiscItems/Kuva" ||
pall[4].StoreItem != "/Lotus/StoreItems/Types/BoosterPacks/RivenModPack"
) {
logger.warn(`self test failed for /Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest`);
}
}

View File

@ -1,65 +0,0 @@
import http from "http";
import https from "https";
import fs from "node:fs";
import { config } from "./configService";
import { logger } from "../utils/logger";
import { app } from "../app";
import { AddressInfo } from "node:net";
let httpServer: http.Server | undefined;
let httpsServer: https.Server | undefined;
const tlsOptions = {
key: fs.readFileSync("static/certs/key.pem"),
cert: fs.readFileSync("static/certs/cert.pem")
};
export const startWebServer = (): void => {
const httpPort = config.httpPort || 80;
const httpsPort = config.httpsPort || 443;
// eslint-disable-next-line @typescript-eslint/no-misused-promises
httpServer = http.createServer(app);
httpServer.listen(httpPort, () => {
logger.info("HTTP server started on port " + httpPort);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
httpsServer = https.createServer(tlsOptions, app);
httpsServer.listen(httpsPort, () => {
logger.info("HTTPS server started on port " + httpsPort);
logger.info(
"Access the WebUI in your browser at http://localhost" + (httpPort == 80 ? "" : ":" + httpPort)
);
});
});
};
export const getWebPorts = (): Record<"http" | "https", number | undefined> => {
return {
http: (httpServer?.address() as AddressInfo | undefined)?.port,
https: (httpsServer?.address() as AddressInfo | undefined)?.port
};
};
export const stopWebServer = async (): Promise<void> => {
const promises: Promise<void>[] = [];
if (httpServer) {
promises.push(
new Promise(resolve => {
httpServer!.close(() => {
resolve();
});
})
);
}
if (httpsServer) {
promises.push(
new Promise(resolve => {
httpsServer!.close(() => {
resolve();
});
})
);
}
await Promise.all(promises);
};

File diff suppressed because it is too large Load Diff

View File

@ -4,11 +4,6 @@ export interface IOid {
$oid: string; $oid: string;
} }
export interface IOidWithLegacySupport {
$oid?: string;
$id?: string;
}
export interface IMongoDate { export interface IMongoDate {
$date: { $date: {
$numberLong: string; $numberLong: string;

View File

@ -1,24 +0,0 @@
import { Types } from "mongoose";
import { IMongoDate, IOidWithLegacySupport } from "./commonTypes";
export interface IFriendInfo {
_id: IOidWithLegacySupport;
DisplayName?: string;
PlatformNames?: string[];
PlatformAccountId?: string;
Status?: number;
ActiveAvatarImageType?: string;
LastLogin?: IMongoDate;
PlayerLevel?: number;
Suffix?: number;
Note?: string;
Favorite?: boolean;
NewRequest?: boolean;
}
export interface IFriendship {
owner: Types.ObjectId;
friend: Types.ObjectId;
Note?: string;
Favorite?: boolean;
}

View File

@ -1,11 +1,10 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IOid, IMongoDate, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { IOid, IMongoDate } from "@/src/types/commonTypes";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPictureFrameInfo } from "./shipTypes"; import { IPictureFrameInfo } from "./shipTypes";
import { IFriendInfo } from "./friendTypes";
export interface IGuildClient { export interface IGuildClient {
_id: IOidWithLegacySupport; _id: IOid;
Name: string; Name: string;
MOTD: string; MOTD: string;
LongMOTD?: ILongMOTD; LongMOTD?: ILongMOTD;
@ -22,7 +21,7 @@ export interface IGuildClient {
CeremonyResetDate?: IMongoDate; CeremonyResetDate?: IMongoDate;
CrossPlatformEnabled?: boolean; CrossPlatformEnabled?: boolean;
AutoContributeFromVault?: boolean; AutoContributeFromVault?: boolean;
AllianceId?: IOidWithLegacySupport; AllianceId?: IOid;
} }
export interface IGuildDatabase { export interface IGuildDatabase {
@ -71,6 +70,7 @@ export interface ILongMOTD {
authorGuildName?: string; authorGuildName?: string;
} }
// 32 seems to be reserved
export enum GuildPermission { export enum GuildPermission {
Ruler = 1, // Clan: Change hierarchy. Alliance (Creator only): Kick clans. Ruler = 1, // Clan: Change hierarchy. Alliance (Creator only): Kick clans.
Advertiser = 8192, Advertiser = 8192,
@ -78,7 +78,6 @@ export enum GuildPermission {
Regulator = 4, // Kick members Regulator = 4, // Kick members
Promoter = 8, // Clan: Promote and demote members. Alliance (Creator only): Change clan permissions. Promoter = 8, // Clan: Promote and demote members. Alliance (Creator only): Change clan permissions.
Architect = 16, // Create and destroy rooms Architect = 16, // Create and destroy rooms
Host = 32, // No longer used in modern versions
Decorator = 1024, // Create and destroy decos Decorator = 1024, // Create and destroy decos
Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault. Treasurer = 64, // Clan: Contribute from vault and edit tax rate. Alliance: Divvy vault.
Tech = 128, // Queue research Tech = 128, // Queue research
@ -105,6 +104,21 @@ export interface IGuildMemberDatabase {
ShipDecorationsContributed?: ITypeCount[]; ShipDecorationsContributed?: ITypeCount[];
} }
export interface IFriendInfo {
_id: IOid;
DisplayName?: string;
PlatformNames?: string[];
PlatformAccountId?: string;
Status?: number;
ActiveAvatarImageType?: string;
LastLogin?: IMongoDate;
PlayerLevel?: number;
Suffix?: number;
Note?: string;
Favorite?: boolean;
NewRequest?: boolean;
}
// GuildMemberInfo // GuildMemberInfo
export interface IGuildMemberClient extends IFriendInfo { export interface IGuildMemberClient extends IFriendInfo {
Rank: number; Rank: number;
@ -127,13 +141,13 @@ export interface IGuildVault {
} }
export interface IDojoClient { export interface IDojoClient {
_id: IOidWithLegacySupport; // ID of the guild _id: IOid; // ID of the guild
Name: string; Name: string;
Tier: number; Tier: number;
TradeTax?: number; TradeTax?: number;
FixedContributions: boolean; FixedContributions: boolean;
DojoRevision: number; DojoRevision: number;
AllianceId?: IOidWithLegacySupport; AllianceId?: IOid;
Vault?: IGuildVault; Vault?: IGuildVault;
Class?: number; // Level Class?: number; // Level
RevisionTime: number; RevisionTime: number;
@ -148,11 +162,11 @@ export interface IDojoClient {
} }
export interface IDojoComponentClient { export interface IDojoComponentClient {
id: IOidWithLegacySupport; id: IOid;
SortId?: IOidWithLegacySupport; SortId?: IOid;
pf: string; // Prefab (.level) pf: string; // Prefab (.level)
ppf: string; ppf: string;
pi?: IOidWithLegacySupport; // Parent ID. N/A to root. pi?: IOid; // Parent ID. N/A to root.
op?: string; // Name of the door within this room that leads to its parent. N/A to root. op?: string; // Name of the door within this room that leads to its parent. N/A to root.
pp?: string; // Name of the door within the parent that leads to this room. N/A to root. pp?: string; // Name of the door within the parent that leads to this room. N/A to root.
Name?: string; Name?: string;
@ -166,7 +180,7 @@ export interface IDojoComponentClient {
DestructionTimeRemaining?: number; // old versions DestructionTimeRemaining?: number; // old versions
Decos?: IDojoDecoClient[]; Decos?: IDojoDecoClient[];
DecoCapacity?: number; DecoCapacity?: number;
PaintBot?: IOidWithLegacySupport; PaintBot?: IOid;
PendingColors?: number[]; PendingColors?: number[];
Colors?: number[]; Colors?: number[];
PendingLights?: number[]; PendingLights?: number[];
@ -191,7 +205,7 @@ export interface IDojoComponentDatabase
} }
export interface IDojoDecoClient { export interface IDojoDecoClient {
id: IOidWithLegacySupport; id: IOid;
Type: string; Type: string;
Pos: number[]; Pos: number[];
Rot: number[]; Rot: number[];
@ -285,7 +299,7 @@ export interface IGuildAdDatabase {
} }
export interface IAllianceClient { export interface IAllianceClient {
_id: IOidWithLegacySupport; _id: IOid;
Name: string; Name: string;
MOTD?: ILongMOTD; MOTD?: ILongMOTD;
LongMOTD?: ILongMOTD; LongMOTD?: ILongMOTD;
@ -306,7 +320,7 @@ export interface IAllianceDatabase {
} }
export interface IAllianceMemberClient { export interface IAllianceMemberClient {
_id: IOidWithLegacySupport; _id: IOid;
Name: string; Name: string;
Tier: number; Tier: number;
Pending: boolean; Pending: boolean;
@ -314,7 +328,7 @@ export interface IAllianceMemberClient {
Permissions: number; Permissions: number;
MemberCount: number; MemberCount: number;
ClanLeader?: string; ClanLeader?: string;
ClanLeaderId?: IOidWithLegacySupport; ClanLeaderId?: IOid;
OriginalPlatform?: number; OriginalPlatform?: number;
} }

View File

@ -1,4 +1,4 @@
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
ICrewShipCustomization, ICrewShipCustomization,
@ -92,7 +92,7 @@ export interface IEquipmentClient
IEquipmentDatabase, IEquipmentDatabase,
"_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details" "_id" | "InfestationDate" | "Expiry" | "UpgradesExpiry" | "UmbraDate" | "CrewMembers" | "Details"
> { > {
ItemId: IOidWithLegacySupport; ItemId: IOid;
InfestationDate?: IMongoDate; InfestationDate?: IMongoDate;
Expiry?: IMongoDate; Expiry?: IMongoDate;
UpgradesExpiry?: IMongoDate; UpgradesExpiry?: IMongoDate;

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IOid, IMongoDate, IOidWithLegacySupport } from "../commonTypes"; import { IOid, IMongoDate } from "../commonTypes";
import { import {
IColor, IColor,
IItemConfig, IItemConfig,
@ -11,8 +11,6 @@ import {
IOperatorConfigDatabase IOperatorConfigDatabase
} from "@/src/types/inventoryTypes/commonInventoryTypes"; } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper"; import { IFingerprintStat, RivenFingerprint } from "@/src/helpers/rivenHelper";
import { IOrbiter } from "../personalRoomsTypes";
import { ICountedStoreItem } from "warframe-public-export-plus";
export type InventoryDatabaseEquipment = { export type InventoryDatabaseEquipment = {
[_ in TEquipmentKey]: IEquipmentDatabase[]; [_ in TEquipmentKey]: IEquipmentDatabase[];
@ -55,7 +53,6 @@ export interface IInventoryDatabase
| "CrewMembers" | "CrewMembers"
| "QualifyingInvasions" | "QualifyingInvasions"
| "LastInventorySync" | "LastInventorySync"
| "EndlessXP"
| TEquipmentKey | TEquipmentKey
>, >,
InventoryDatabaseEquipment { InventoryDatabaseEquipment {
@ -94,7 +91,6 @@ export interface IInventoryDatabase
CrewMembers: ICrewMemberDatabase[]; CrewMembers: ICrewMemberDatabase[];
QualifyingInvasions: IInvasionProgressDatabase[]; QualifyingInvasions: IInvasionProgressDatabase[];
LastInventorySync?: Types.ObjectId; LastInventorySync?: Types.ObjectId;
EndlessXP?: IEndlessXpProgressDatabase[];
} }
export interface IQuestKeyDatabase { export interface IQuestKeyDatabase {
@ -296,7 +292,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
SortieRewardAttenuation?: ISortieRewardAttenuation[]; SortieRewardAttenuation?: ISortieRewardAttenuation[];
Drones: IDroneClient[]; Drones: IDroneClient[];
StepSequencers: IStepSequencer[]; StepSequencers: IStepSequencer[];
ActiveAvatarImageType?: string; ActiveAvatarImageType: string;
ShipDecorations: ITypeCount[]; ShipDecorations: ITypeCount[];
DiscoveredMarkers: IDiscoveredMarker[]; DiscoveredMarkers: IDiscoveredMarker[];
//CompletedJobs: ICompletedJob[]; //CompletedJobs: ICompletedJob[];
@ -359,7 +355,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
PendingCoupon?: IPendingCouponClient; PendingCoupon?: IPendingCouponClient;
Harvestable: boolean; Harvestable: boolean;
DeathSquadable: boolean; DeathSquadable: boolean;
EndlessXP?: IEndlessXpProgressClient[]; EndlessXP?: IEndlessXpProgress[];
DialogueHistory?: IDialogueHistoryClient; DialogueHistory?: IDialogueHistoryClient;
CalendarProgress?: ICalendarProgress; CalendarProgress?: ICalendarProgress;
SongChallenges?: ISongChallenge[]; SongChallenges?: ISongChallenge[];
@ -374,10 +370,9 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
EchoesHexConquestCacheScoreMission?: number; EchoesHexConquestCacheScoreMission?: number;
EchoesHexConquestActiveFrameVariants?: string[]; EchoesHexConquestActiveFrameVariants?: string[];
EchoesHexConquestActiveStickers?: string[]; EchoesHexConquestActiveStickers?: string[];
BrandedSuits?: IOidWithLegacySupport[]; BrandedSuits?: IOid[];
LockedWeaponGroup?: ILockedWeaponGroupClient; LockedWeaponGroup?: ILockedWeaponGroupClient;
HubNpcCustomizations?: IHubNpcCustomization[]; HubNpcCustomizations?: IHubNpcCustomization[];
Ship?: IOrbiter; // U22 and below, response only
} }
export interface IAffiliation { export interface IAffiliation {
@ -535,16 +530,6 @@ export interface IUpgradeDatabase extends Omit<IUpgradeClient, "ItemId"> {
_id: Types.ObjectId; _id: Types.ObjectId;
} }
export interface IUpgradeFromClient {
ItemType: string;
ItemId: IOidWithLegacySupport;
FromSKU?: boolean;
UpgradeFingerprint: string;
PendingRerollFingerprint: string;
ItemCount: number;
LastAdded: IOidWithLegacySupport;
}
export interface ICrewShipMembersClient { export interface ICrewShipMembersClient {
SLOT_A?: ICrewShipMemberClient; SLOT_A?: ICrewShipMemberClient;
SLOT_B?: ICrewShipMemberClient; SLOT_B?: ICrewShipMemberClient;
@ -573,7 +558,7 @@ export interface ICrewShipCustomization {
export interface IShipExterior { export interface IShipExterior {
SkinFlavourItem?: string; SkinFlavourItem?: string;
Colors?: IColor; Colors: IColor;
ShipAttachments?: IShipAttachments; ShipAttachments?: IShipAttachments;
} }
@ -765,8 +750,7 @@ export interface IKubrowPetDetailsClient extends Omit<IKubrowPetDetailsDatabase,
export enum Status { export enum Status {
StatusAvailable = "STATUS_AVAILABLE", StatusAvailable = "STATUS_AVAILABLE",
StatusStasis = "STATUS_STASIS", StatusStasis = "STATUS_STASIS"
StatusIncubating = "STATUS_INCUBATING"
} }
export interface ILastSortieRewardClient { export interface ILastSortieRewardClient {
@ -864,8 +848,6 @@ export interface IMission extends IMissionDatabase {
RewardsCooldownTime?: IMongoDate; RewardsCooldownTime?: IMongoDate;
} }
export type TNemesisFaction = "FC_GRINEER" | "FC_CORPUS" | "FC_INFESTATION";
export interface INemesisBaseClient { export interface INemesisBaseClient {
fp: bigint | number; fp: bigint | number;
manifest: string; manifest: string;
@ -875,7 +857,7 @@ export interface INemesisBaseClient {
WeaponIdx: number; WeaponIdx: number;
AgentIdx: number; AgentIdx: number;
BirthNode: string; BirthNode: string;
Faction: TNemesisFaction; Faction: string;
Rank: number; Rank: number;
k: boolean; k: boolean;
Traded: boolean; Traded: boolean;
@ -930,14 +912,10 @@ export interface IPendingRecipeDatabase {
Pistols?: IEquipmentDatabase[]; Pistols?: IEquipmentDatabase[];
Melee?: IEquipmentDatabase[]; Melee?: IEquipmentDatabase[];
SuitToUnbrand?: Types.ObjectId; SuitToUnbrand?: Types.ObjectId;
KubrowPet?: Types.ObjectId;
} }
export interface IPendingRecipeClient export interface IPendingRecipeClient
extends Omit< extends Omit<IPendingRecipeDatabase, "CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand"> {
IPendingRecipeDatabase,
"CompletionDate" | "LongGuns" | "Pistols" | "Melee" | "SuitToUnbrand" | "KubrowPet"
> {
CompletionDate: IMongoDate; CompletionDate: IMongoDate;
} }
@ -1070,7 +1048,7 @@ export interface IQuestStage {
export interface IRawUpgrade { export interface IRawUpgrade {
ItemType: string; ItemType: string;
ItemCount: number; ItemCount: number;
LastAdded?: IOidWithLegacySupport; LastAdded?: IOid;
} }
export interface ISeasonChallenge { export interface ISeasonChallenge {
@ -1163,24 +1141,9 @@ export interface IEvolutionProgress {
export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD"; export type TEndlessXpCategory = "EXC_NORMAL" | "EXC_HARD";
export interface IEndlessXpProgressDatabase { export interface IEndlessXpProgress {
Category: TEndlessXpCategory; Category: TEndlessXpCategory;
Earn: number;
Claim: number;
BonusAvailable?: Date;
Expiry?: Date;
Choices: string[]; Choices: string[];
PendingRewards: IEndlessXpReward[];
}
export interface IEndlessXpProgressClient extends Omit<IEndlessXpProgressDatabase, "BonusAvailable" | "Expiry"> {
BonusAvailable?: IMongoDate;
Expiry?: IMongoDate;
}
export interface IEndlessXpReward {
RequiredTotalXp: number;
Rewards: ICountedStoreItem[];
} }
export interface IDialogueHistoryClient { export interface IDialogueHistoryClient {

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