2024-06-20 11:47:21 +02:00
|
|
|
import { RequestHandler } from "express";
|
2025-03-15 06:39:54 -07:00
|
|
|
import { getAccountIdForRequest } from "@/src/services/loginService";
|
2025-01-25 13:12:49 +01:00
|
|
|
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
|
2024-05-15 21:55:59 +02:00
|
|
|
import { config } from "@/src/services/configService";
|
2024-07-03 12:31:35 +02:00
|
|
|
import allDialogue from "@/static/fixed_responses/allDialogue.json";
|
2023-12-14 17:34:15 +01:00
|
|
|
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
|
2025-01-20 12:19:32 +01:00
|
|
|
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
|
2025-01-15 05:20:30 +01:00
|
|
|
import { IPolarity, ArtifactPolarity, EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
|
2024-12-29 21:11:36 +01:00
|
|
|
import {
|
|
|
|
ExportCustoms,
|
|
|
|
ExportFlavour,
|
|
|
|
ExportRegions,
|
|
|
|
ExportResources,
|
|
|
|
ExportVirtuals
|
|
|
|
} from "warframe-public-export-plus";
|
2025-02-22 11:09:17 -08:00
|
|
|
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController";
|
2025-03-15 06:39:54 -07:00
|
|
|
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
|
|
|
|
import { logger } from "@/src/utils/logger";
|
2024-12-29 21:11:36 +01:00
|
|
|
|
|
|
|
export const inventoryController: RequestHandler = async (request, response) => {
|
2025-03-15 06:39:54 -07:00
|
|
|
const accountId = await getAccountIdForRequest(request);
|
2023-06-04 03:06:22 +02:00
|
|
|
|
2025-03-15 06:39:54 -07:00
|
|
|
const inventory = await Inventory.findOne({ accountOwnerId: accountId });
|
2023-06-04 03:06:22 +02:00
|
|
|
|
|
|
|
if (!inventory) {
|
|
|
|
response.status(400).json({ error: "inventory was undefined" });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-12-22 00:44:49 +01:00
|
|
|
// Handle daily reset
|
2025-03-15 06:39:54 -07:00
|
|
|
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
|
2025-01-17 07:02:19 +01:00
|
|
|
for (const key of allDailyAffiliationKeys) {
|
|
|
|
inventory[key] = 16000 + inventory.PlayerLevel * 500;
|
|
|
|
}
|
2025-01-05 05:17:56 +01:00
|
|
|
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
|
2025-02-25 17:31:52 -08:00
|
|
|
|
|
|
|
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
|
|
|
|
|
2025-03-15 06:39:54 -07:00
|
|
|
if (inventory.NextRefill) {
|
|
|
|
if (config.noArgonCrystalDecay) {
|
|
|
|
inventory.FoundToday = undefined;
|
|
|
|
} else {
|
|
|
|
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
|
|
|
|
const today = Math.trunc(Date.now() / 86400000);
|
|
|
|
const daysPassed = today - lastLoginDay;
|
|
|
|
for (let i = 0; i != daysPassed; ++i) {
|
|
|
|
const numArgonCrystals =
|
|
|
|
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
|
|
|
|
?.ItemCount ?? 0;
|
|
|
|
if (numArgonCrystals == 0) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const numStableArgonCrystals =
|
|
|
|
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
|
|
|
|
?.ItemCount ?? 0;
|
|
|
|
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
|
|
|
|
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
|
|
|
|
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
|
|
|
|
numArgonCrystals,
|
|
|
|
numStableArgonCrystals,
|
|
|
|
numDecayingArgonCrystals,
|
|
|
|
numDecayingArgonCrystalsToRemove
|
|
|
|
});
|
|
|
|
// Remove half of owned decaying argon crystals
|
|
|
|
addMiscItems(inventory, [
|
|
|
|
{
|
|
|
|
ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal",
|
|
|
|
ItemCount: numDecayingArgonCrystalsToRemove * -1
|
|
|
|
}
|
|
|
|
]);
|
|
|
|
// All stable argon crystals are now decaying
|
|
|
|
inventory.FoundToday = undefined;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
|
2024-12-22 00:44:49 +01:00
|
|
|
await inventory.save();
|
|
|
|
}
|
|
|
|
|
2025-01-03 05:22:56 +01:00
|
|
|
if (
|
|
|
|
inventory.InfestedFoundry &&
|
|
|
|
inventory.InfestedFoundry.AbilityOverrideUnlockCooldown &&
|
|
|
|
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
|
|
|
|
) {
|
2025-01-03 22:17:34 +01:00
|
|
|
handleSubsumeCompletion(inventory);
|
2025-01-03 05:22:56 +01:00
|
|
|
await inventory.save();
|
|
|
|
}
|
|
|
|
|
2025-01-25 13:12:49 +01:00
|
|
|
response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
|
|
|
|
};
|
|
|
|
|
|
|
|
export const getInventoryResponse = async (
|
|
|
|
inventory: TInventoryDatabaseDocument,
|
|
|
|
xpBasedLevelCapDisabled: boolean
|
|
|
|
): Promise<IInventoryClient> => {
|
2025-01-03 22:17:34 +01:00
|
|
|
const inventoryWithLoadOutPresets = await inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>(
|
|
|
|
"LoadOutPresets"
|
|
|
|
);
|
|
|
|
const inventoryWithLoadOutPresetsAndShips = await inventoryWithLoadOutPresets.populate<{ Ships: IShipInventory }>(
|
|
|
|
"Ships"
|
|
|
|
);
|
2025-01-20 12:19:32 +01:00
|
|
|
const inventoryResponse = inventoryWithLoadOutPresetsAndShips.toJSON<IInventoryClient>();
|
2023-06-04 03:06:22 +02:00
|
|
|
|
2024-12-22 00:34:19 +01:00
|
|
|
if (config.infiniteCredits) {
|
2024-05-02 23:37:51 +02:00
|
|
|
inventoryResponse.RegularCredits = 999999999;
|
2024-12-22 00:34:19 +01:00
|
|
|
}
|
|
|
|
if (config.infinitePlatinum) {
|
2025-03-09 11:15:33 -07:00
|
|
|
inventoryResponse.PremiumCreditsFree = 0;
|
2024-05-02 23:37:51 +02:00
|
|
|
inventoryResponse.PremiumCredits = 999999999;
|
|
|
|
}
|
2025-01-06 05:36:39 +01:00
|
|
|
if (config.infiniteEndo) {
|
|
|
|
inventoryResponse.FusionPoints = 999999999;
|
|
|
|
}
|
|
|
|
if (config.infiniteRegalAya) {
|
|
|
|
inventoryResponse.PrimeTokens = 999999999;
|
|
|
|
}
|
2024-05-02 23:37:51 +02:00
|
|
|
|
2024-07-03 12:31:35 +02:00
|
|
|
if (config.skipAllDialogue) {
|
|
|
|
inventoryResponse.TauntHistory = [
|
|
|
|
{
|
|
|
|
node: "TreasureTutorial",
|
|
|
|
state: "TS_COMPLETED"
|
|
|
|
}
|
|
|
|
];
|
|
|
|
for (const str of allDialogue) {
|
|
|
|
addString(inventoryResponse.NodeIntrosCompleted, str);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-07 23:50:38 +02:00
|
|
|
if (config.unlockAllMissions) {
|
2024-12-20 03:11:38 +01:00
|
|
|
inventoryResponse.Missions = [];
|
|
|
|
for (const tag of Object.keys(ExportRegions)) {
|
|
|
|
inventoryResponse.Missions.push({
|
|
|
|
Completes: 1,
|
|
|
|
Tier: 1,
|
|
|
|
Tag: tag
|
|
|
|
});
|
|
|
|
}
|
2024-07-03 12:31:35 +02:00
|
|
|
addString(inventoryResponse.NodeIntrosCompleted, "TeshinHardModeUnlocked");
|
2024-05-07 23:50:38 +02:00
|
|
|
}
|
|
|
|
|
2024-06-18 23:10:26 +02:00
|
|
|
if (config.unlockAllShipDecorations) {
|
|
|
|
inventoryResponse.ShipDecorations = [];
|
|
|
|
for (const [uniqueName, item] of Object.entries(ExportResources)) {
|
|
|
|
if (item.productCategory == "ShipDecorations") {
|
|
|
|
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.unlockAllFlavourItems) {
|
|
|
|
inventoryResponse.FlavourItems = [];
|
|
|
|
for (const uniqueName in ExportFlavour) {
|
|
|
|
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
|
|
|
|
}
|
|
|
|
}
|
2024-02-18 13:58:43 +01:00
|
|
|
|
2024-05-29 22:08:41 +02:00
|
|
|
if (config.unlockAllSkins) {
|
2025-01-20 12:19:32 +01:00
|
|
|
const missingWeaponSkins = new Set(Object.keys(ExportCustoms));
|
|
|
|
inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType));
|
|
|
|
for (const uniqueName of missingWeaponSkins) {
|
2024-05-29 22:08:41 +02:00
|
|
|
inventoryResponse.WeaponSkins.push({
|
|
|
|
ItemId: {
|
2025-01-12 02:42:27 +01:00
|
|
|
$oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0")
|
2024-05-29 22:08:41 +02:00
|
|
|
},
|
2024-06-18 23:10:26 +02:00
|
|
|
ItemType: uniqueName
|
2024-05-29 22:08:41 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-12-29 21:11:36 +01:00
|
|
|
if (config.unlockAllCapturaScenes) {
|
|
|
|
for (const uniqueName of Object.keys(ExportResources)) {
|
|
|
|
if (resourceInheritsFrom(uniqueName, "/Lotus/Types/Items/MiscItems/PhotoboothTile")) {
|
|
|
|
inventoryResponse.MiscItems.push({
|
|
|
|
ItemType: uniqueName,
|
|
|
|
ItemCount: 1
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-16 12:24:15 +02:00
|
|
|
if (typeof config.spoofMasteryRank === "number" && config.spoofMasteryRank >= 0) {
|
2024-05-28 13:28:35 +02:00
|
|
|
inventoryResponse.PlayerLevel = config.spoofMasteryRank;
|
2025-01-24 21:09:34 +01:00
|
|
|
if (!xpBasedLevelCapDisabled) {
|
2024-06-16 12:24:15 +02:00
|
|
|
// This client has not been patched to accept any mastery rank, need to fake the XP.
|
|
|
|
inventoryResponse.XPInfo = [];
|
|
|
|
let numFrames = getExpRequiredForMr(Math.min(config.spoofMasteryRank, 5030)) / 6000;
|
|
|
|
while (numFrames-- > 0) {
|
|
|
|
inventoryResponse.XPInfo.push({
|
|
|
|
ItemType: "/Lotus/Powersuits/Mag/Mag",
|
|
|
|
XP: 1_600_000
|
|
|
|
});
|
|
|
|
}
|
2024-05-28 13:28:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-22 23:22:38 +02:00
|
|
|
if (config.universalPolarityEverywhere) {
|
|
|
|
const Polarity: IPolarity[] = [];
|
2025-02-03 13:21:12 -08:00
|
|
|
for (let i = 0; i != 12; ++i) {
|
2024-06-22 23:22:38 +02:00
|
|
|
Polarity.push({
|
|
|
|
Slot: i,
|
|
|
|
Value: ArtifactPolarity.Any
|
|
|
|
});
|
|
|
|
}
|
|
|
|
for (const key of equipmentKeys) {
|
|
|
|
if (key in inventoryResponse) {
|
|
|
|
for (const equipment of inventoryResponse[key]) {
|
|
|
|
equipment.Polarity = Polarity;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-15 05:20:30 +01:00
|
|
|
if (config.unlockDoubleCapacityPotatoesEverywhere) {
|
|
|
|
for (const key of equipmentKeys) {
|
|
|
|
if (key in inventoryResponse) {
|
|
|
|
for (const equipment of inventoryResponse[key]) {
|
|
|
|
equipment.Features ??= 0;
|
|
|
|
equipment.Features |= EquipmentFeatures.DOUBLE_CAPACITY;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.unlockExilusEverywhere) {
|
|
|
|
for (const key of equipmentKeys) {
|
|
|
|
if (key in inventoryResponse) {
|
|
|
|
for (const equipment of inventoryResponse[key]) {
|
|
|
|
equipment.Features ??= 0;
|
|
|
|
equipment.Features |= EquipmentFeatures.UTILITY_SLOT;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.unlockArcanesEverywhere) {
|
|
|
|
for (const key of equipmentKeys) {
|
|
|
|
if (key in inventoryResponse) {
|
|
|
|
for (const equipment of inventoryResponse[key]) {
|
|
|
|
equipment.Features ??= 0;
|
|
|
|
equipment.Features |= EquipmentFeatures.ARCANE_SLOT;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-01-17 07:02:19 +01:00
|
|
|
if (config.noDailyStandingLimits) {
|
|
|
|
for (const key of allDailyAffiliationKeys) {
|
|
|
|
inventoryResponse[key] = 999_999;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-22 11:09:17 -08:00
|
|
|
if (inventoryResponse.InfestedFoundry) {
|
|
|
|
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
|
|
|
|
}
|
|
|
|
|
2024-10-07 17:44:02 +02:00
|
|
|
// This determines if the "void fissures" tab is shown in navigation.
|
|
|
|
inventoryResponse.HasOwnedVoidProjectionsPreviously = true;
|
|
|
|
|
2025-02-02 05:16:43 -08:00
|
|
|
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
|
|
|
|
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
|
2025-01-25 13:12:49 +01:00
|
|
|
|
2025-03-15 03:21:26 -07:00
|
|
|
// Set 2FA enabled so trading post can be used
|
|
|
|
inventoryResponse.HWIDProtectEnabled = true;
|
|
|
|
|
2025-01-25 13:12:49 +01:00
|
|
|
return inventoryResponse;
|
2023-05-19 15:22:48 -03:00
|
|
|
};
|
|
|
|
|
2025-02-18 17:14:42 -08:00
|
|
|
export const addString = (arr: string[], str: string): void => {
|
2024-07-03 12:31:35 +02:00
|
|
|
if (!arr.find(x => x == str)) {
|
|
|
|
arr.push(str);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-05-28 13:28:35 +02:00
|
|
|
const getExpRequiredForMr = (rank: number): number => {
|
|
|
|
if (rank <= 30) {
|
|
|
|
return 2500 * rank * rank;
|
|
|
|
}
|
|
|
|
return 2_250_000 + 147_500 * (rank - 30);
|
|
|
|
};
|
|
|
|
|
2024-12-29 21:11:36 +01:00
|
|
|
const resourceInheritsFrom = (resourceName: string, targetName: string): boolean => {
|
|
|
|
let parentName = resourceGetParent(resourceName);
|
|
|
|
for (; parentName != undefined; parentName = resourceGetParent(parentName)) {
|
|
|
|
if (parentName == targetName) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
|
|
|
const resourceGetParent = (resourceName: string): string | undefined => {
|
|
|
|
if (resourceName in ExportResources) {
|
|
|
|
return ExportResources[resourceName].parentName;
|
|
|
|
}
|
2025-03-15 03:24:39 -07:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
2024-12-29 21:11:36 +01:00
|
|
|
return ExportVirtuals[resourceName]?.parentName;
|
|
|
|
};
|
2025-01-12 02:42:27 +01:00
|
|
|
|
|
|
|
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
|
|
|
|
const catBreadHash = (name: string): number => {
|
|
|
|
let hash = 2166136261;
|
|
|
|
for (let i = 0; i != name.length; ++i) {
|
|
|
|
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
|
|
|
|
hash = (hash * 16777619) & 0x7fffffff;
|
|
|
|
}
|
|
|
|
return hash;
|
|
|
|
};
|