import type { Request, RequestHandler } from "express";
import { getAccountForRequest, getBuildLabel } from "../../services/loginService.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { config } from "../../services/configService.ts";
import allDialogue from "../../../static/fixed_responses/allDialogue.json" with { type: "json" };
import allPopups from "../../../static/fixed_responses/allPopups.json" with { type: "json" };
import type { ILoadoutConfigClientLegacy, ILoadoutDatabase, ILoadOutPresets } from "../../types/saveLoadoutTypes.ts";
import type {
IFusionTreasure,
IFusionTreasureClientLegacy,
IInventoryClient,
IShipInventory,
IUpgradeClient
} from "../../types/inventoryTypes/inventoryTypes.ts";
import {
accountCheatBooleans,
accountCheatNumbers,
eLoadoutIndex,
equipmentKeys,
loadoutKeysLegacy
} from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import type { ICountedItem } from "warframe-public-export-plus";
import { ExportArcanes, ExportResources, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "../../services/infestedFoundryService.ts";
import {
addEmailItem,
addItem,
addMiscItems,
addMods,
allDailyAffiliationKeys,
checkCalendarAutoAdvance,
cleanupInventory,
createLibraryDailyTask,
dispatchPendingPremiumCredits,
ensureUserHasFounderHonoria,
ensureUserHasSteelPathRewards,
getCalendarProgress,
getInventory,
handleTauMemories,
PRE_U40_MAX_KUBROW_EGGS
} from "../../services/inventoryService.ts";
import { logger } from "../../utils/logger.ts";
import { addString, catBreadHash } from "../../helpers/stringHelpers.ts";
import { Types, type FlattenMaps } from "mongoose";
import { getNemesisManifest } from "../../helpers/nemesisHelpers.ts";
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
import { Ship } from "../../models/shipModel.ts";
import {
convertIColorToLegacyColors,
convertIColorToLegacyColorsWithAtt,
convertToLegacyFingerprint,
fromOid,
modernToU5Recipes,
toLegacyDate,
toLegacyOid,
toMongoDate2,
toOid,
toOid2,
U5Recipes,
version_compare
} from "../../helpers/inventoryHelpers.ts";
import { Inbox } from "../../models/inboxModel.ts";
import { unixTimesInMs } from "../../constants/timeConstants.ts";
import { DailyDeal } from "../../models/worldStateModel.ts";
import { eEquipmentFeatures } from "../../types/equipmentTypes.ts";
import { generateRewardSeed } from "../../services/rngService.ts";
import { getInvasionByOid, getWorldState } from "../../services/worldStateService.ts";
import { createMessage, getInboxFilter } from "../../services/inboxService.ts";
import gameToBuildVersion from "../../constants/gameToBuildVersion.ts";
import { PendingTrade } from "../../models/tradingModel.ts";
import { exportTrade } from "../../services/tradingService.ts";
import { supplementalSuits } from "../../services/itemDataService.ts";
import suitDefaultUpgrades from "../../constants/suitDefaultUpgrades.ts";
import type { ITypeCount } from "../../types/commonTypes.ts";
import { sendWsBroadcastToWebui } from "../../services/wsService.ts";
import { wikiDateToBuildVersion } from "../../helpers/versionHelper.ts";
export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request);
const buildLabel = getBuildLabel(request, account);
const inventory = await getInventory(account._id, undefined);
let sendUpdateForWebui = false;
// Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
const today = Math.trunc(Date.now() / 86400000);
for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500;
}
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.TradesRemaining = inventory.PlayerLevel;
if (inventory.Founder) {
inventory.TradesRemaining += 2;
}
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(buildLabel);
if (inventory.NextRefill) {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const daysPassed = today - lastLoginDay;
if (!inventory.noArgonCrystalDecay) {
let numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0;
for (let i = 0; i != daysPassed; ++i) {
if (numArgonCrystals == 0) {
break;
}
const numStableArgonCrystals = Math.min(
numArgonCrystals,
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0
);
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
numArgonCrystals,
numStableArgonCrystals,
numDecayingArgonCrystals,
numDecayingArgonCrystalsToRemove
});
// Remove half of owned decaying argon crystals
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal",
ItemCount: numDecayingArgonCrystalsToRemove * -1
}
]);
numArgonCrystals -= numDecayingArgonCrystalsToRemove;
// All stable argon crystals are now decaying
inventory.FoundToday = undefined;
}
}
if (inventory.UsedDailyDeals.length != 0) {
if (daysPassed == 1) {
const todayAt0Utc = today * 86400000;
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
const darvoOid =
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
const deal = await DailyDeal.findById(darvoOid);
if (deal) {
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
} else {
inventory.UsedDailyDeals = [];
}
} else {
inventory.UsedDailyDeals = [];
}
}
for (const suit of inventory.Suits) {
if (suit.ExtraRemaining !== undefined) {
suit.ExtraRemaining = undefined;
}
}
}
// TODO: Setup CalendarProgress as part of 1999 mission completion?
const previousYearIteration = inventory.CalendarProgress?.Iteration;
// We need to do the following to ensure the in-game calendar does not break:
getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364)
checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424)
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress!.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues &&
!inventory.RetroDisableKissInboxMessage
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail, {}, buildLabel);
kalymos = false;
break;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem", {}, buildLabel);
}
}
if (inventory.Founder) {
await ensureUserHasFounderHonoria(inventory);
}
await ensureUserHasSteelPathRewards(inventory);
cleanupInventory(inventory);
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
//await inventory.save();
}
if (
inventory.InfestedFoundry &&
inventory.InfestedFoundry.AbilityOverrideUnlockCooldown &&
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) {
handleSubsumeCompletion(inventory);
//await inventory.save();
}
const forWebui = "wsid" in request.query;
if (inventory.pendingPremiumCredits && !forWebui) {
await dispatchPendingPremiumCredits(inventory);
//await inventory.save();
}
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: readonly ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
}
if (factionSidedWith && battlePay) {
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Language/Menu/GrineerInvasionLeader"
: "/Lotus/Language/Menu/CorpusInvasionLeader",
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
}
]);
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
{
const now = Date.now();
const lastSyncDuviriMood = Math.trunc((inventory.duviriSeedRefresh?.getTime() ?? 0) / 7200000);
const currentDuviriMood = Math.trunc(now / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
if (!inventory.DuviriInfo) {
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
inventory.duviriSeedRefresh = new Date(now);
} else {
inventory.DuviriInfo.Seed = generateRewardSeed();
inventory.duviriSeedRefresh = new Date(now);
}
}
}
if (inventory.QuestKeys.some(x => x.ItemType.endsWith("KubrowQuestKeyChain") && x.Completed)) {
for (const item of inventory.KubrowPets) {
item.Details.HasCollar = true;
}
}
await handleTauMemories(inventory);
if (
inventory.QuestKeys.some(
q => q.ItemType === "/Lotus/Types/Keys/JadeShadows/JadeShadowQuestKeyChain" && q.Completed == true
) &&
!inventory.NodeIntrosCompleted.some(i => i.startsWith("BabyName_"))
) {
inventory.NodeIntrosCompleted.push("BabyName_A");
logger.debug(`forcing baby name to Sirius`);
}
if (version_compare(buildLabel, gameToBuildVersion["15.0.0"]) < 0) {
const suitTypes = inventory.Suits.map(s => s.ItemType);
const rawUpgradeTypes = inventory.RawUpgrades.map(s => s.ItemType);
const upgradeTypes = inventory.Upgrades.map(s => s.ItemType);
const blacklist = new Set([...rawUpgradeTypes, ...upgradeTypes]);
const seen = new Set<string>();
const modsToAdd: ITypeCount[] = suitTypes.flatMap(suit =>
(suitDefaultUpgrades[suit] ?? []).flatMap(u => {
const itemType = u.ItemType;
if (blacklist.has(itemType) || seen.has(itemType)) return [];
seen.add(itemType);
return [
{
ItemType: itemType,
ItemCount: 1
}
];
})
);
if (modsToAdd.length > 0) {
logger.debug(`adding ${modsToAdd.length} missing legacy ability mods`);
addMods(inventory, modsToAdd);
sendUpdateForWebui = true;
}
}
if (inventory.isModified()) {
await inventory.save();
if (sendUpdateForWebui) sendWsBroadcastToWebui({ update_inventory: true }, account._id.toString());
}
response.json(
await getInventoryResponse(request, inventory, "xpBasedLevelCapDisabled" in request.query, buildLabel, forWebui)
);
};
export const getInventoryResponse = async (
request: Request,
inventory: TInventoryDatabaseDocument,
xpBasedLevelCapDisabled: boolean,
buildLabel: string,
forWebui: boolean = false
): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships, latestMessage, pendingTrades] = await Promise.all([
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
Ship.find({ ShipOwnerId: inventory.accountOwnerId }),
Inbox.findOne(getInboxFilter(inventory.accountOwnerId, buildLabel), "_id").sort({ date: -1 }),
PendingTrade.find({ $or: [{ a: inventory.accountOwnerId }, { b: inventory.accountOwnerId }] })
]);
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
inventoryResponse.PendingTrades = pendingTrades.map(trade => {
const us = trade.a.equals(inventory.accountOwnerId) ? "a" : "b";
return exportTrade(trade, us);
});
if (!forWebui) {
for (const key of accountCheatBooleans) {
delete inventoryResponse[key as keyof typeof inventoryResponse];
}
for (const key of accountCheatNumbers) {
delete inventoryResponse[key as keyof typeof inventoryResponse];
}
}
inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
if (latestMessage) {
inventoryResponse.Mailbox = {
LastInboxId: toOid(latestMessage._id)
};
}
if (inventory.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999;
}
if (inventory.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 0;
inventoryResponse.PremiumCredits = 999999999;
}
if (inventory.infiniteEndo) {
inventoryResponse.FusionPoints = 999999999;
}
if (inventory.infiniteDirac) {
inventoryResponse.CrewShipFusionPoints = 999999999;
}
if (inventory.infiniteRegalAya) {
inventoryResponse.PrimeTokens = 999999999;
}
if (inventory.infiniteRevives) {
for (const suit of inventoryResponse.Suits) {
suit.ExtraRemaining = 999999999;
}
}
if (inventory.infiniteTrades) {
inventoryResponse.TradesRemaining = 9999;
}
if (inventory.infiniteGifts) {
inventoryResponse.GiftsRemaining = 9999;
}
if (inventory.skipAllDialogue) {
inventoryResponse.TauntHistory ??= [];
if (!inventoryResponse.TauntHistory.find(x => x.node == "TreasureTutorial")) {
inventoryResponse.TauntHistory.push({
node: "TreasureTutorial",
state: "TS_COMPLETED"
});
}
if (!inventoryResponse.TauntHistory.find(x => x.node == "StarchartTutorial")) {
inventoryResponse.TauntHistory.push({
node: "StarchartTutorial",
state: "TS_COMPLETED"
});
}
if (!inventoryResponse.TauntHistory.find(x => x.node == "CraftingTutorial")) {
inventoryResponse.TauntHistory.push({
node: "CraftingTutorial",
state: "TS_COMPLETED"
});
}
for (const str of allDialogue) {
addString(inventoryResponse.NodeIntrosCompleted, str);
}
}
if (inventory.skipAllPopups) {
for (const str of allPopups) {
addString(inventoryResponse.NodeIntrosCompleted, str);
}
inventoryResponse.Settings ??= {
FriendInvRestriction: "GIFT_MODE_ALL",
GiftMode: "GIFT_MODE_ALL",
GuildInvRestriction: "GIFT_MODE_ALL",
ShowFriendInvNotifications: true,
TradingRulesConfirmed: false
};
inventoryResponse.Settings.TradingRulesConfirmed = true;
inventoryResponse.PlayedParkourTutorial = true; // Skips PoE update popup in U22
inventoryResponse.MadeStoryModeDecision = true; // Skips tutorial popup in U15
inventoryResponse.HasResetAccount = true; // In ~U10, you are asked to confirm dojo contributions 3 times because "you will forfeit your one-time reset"
}
if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2026EarlyAccess"
].forEach(uniqueName => {
if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
});
}
if (inventory.spoofMasteryRank && inventory.spoofMasteryRank >= 0) {
inventoryResponse.PlayerLevel = inventory.spoofMasteryRank;
if (!xpBasedLevelCapDisabled) {
// This client has not been patched to accept any mastery rank, need to fake the XP.
// Limiting this to MR 100 to avoid breakage: https://onlyg.it/OpenWF/SpaceNinjaServer/issues/4244
inventoryResponse.XPInfo = [];
let numFrames = getExpRequiredForMr(Math.min(inventory.spoofMasteryRank, 100)) / (30 * 200);
while (numFrames-- > 0) {
inventoryResponse.XPInfo.push({
ItemType: "/Lotus/Powersuits/Mag/Mag",
XP: 900_000 // Enough for rank 30 as per https://wiki.warframe.com/w/Affinity
});
}
}
}
if (inventory.universalPolarityEverywhere) {
const Polarity: IPolarity[] = [];
// 12 is needed for necramechs. 15 is needed for plexus/crewshipharness.
for (let i = 0; i != 15; ++i) {
Polarity.push({
Slot: i,
Value: "AP_ANY"
});
}
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Polarity = Polarity;
}
}
}
}
if (inventory.unlockDoubleCapacityPotatoesEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= eEquipmentFeatures.DOUBLE_CAPACITY;
}
}
}
}
if (inventory.unlockExilusEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= eEquipmentFeatures.UTILITY_SLOT;
}
}
}
}
if (inventory.unlockArcanesEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= eEquipmentFeatures.ARCANE_SLOT;
equipment.Features |= eEquipmentFeatures.SECOND_ARCANE_SLOT;
}
}
}
}
if (inventory.noDailyStandingLimits) {
const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500);
for (const key of allDailyAffiliationKeys) {
inventoryResponse[key] = spoofedDailyAffiliation;
}
}
if (inventory.noDailyFocusLimit) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventory, inventoryResponse.InfestedFoundry);
}
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
if (!forWebui) {
// Ensure numbers comfortably fit in a 32-bit integer so the client's in-memory value doesn't overflow or underflow.
for (const obj of inventory.RawUpgrades) {
if (obj.ItemCount > 999_999_999) {
obj.ItemCount = 999_999_999;
} else if (obj.ItemCount < -999_999_999) {
obj.ItemCount = -999_999_999;
}
}
}
// Convert fusion treasures to client format
inventoryResponse.FusionTreasures = [];
if (!buildLabel || version_compare(buildLabel, gameToBuildVersion["25.7.0"]) >= 0) {
for (const treasure of inventory.HybridFusionTreasures) {
const entry = (inventoryResponse.FusionTreasures as IFusionTreasure[]).find(
x => x.ItemType == treasure.ItemType && x.Sockets == treasure.Sockets
);
if (entry) {
++entry.ItemCount;
} else {
(inventoryResponse.FusionTreasures as IFusionTreasure[]).push({
ItemType: treasure.ItemType,
ItemCount: 1,
Sockets: treasure.Sockets
});
}
}
} else {
for (const treasure of inventory.HybridFusionTreasures) {
const meta = ExportResources[treasure.ItemType];
const sockets: Record<string, string> = {};
for (let i = 0; i != meta.sockets!.length; ++i) {
if ((treasure.Sockets >> i) & 1) {
sockets[String.fromCharCode(0x61 + i)] = meta.sockets![i];
}
}
(inventoryResponse.FusionTreasures as IFusionTreasureClientLegacy[]).push({
ItemId: toOid2(treasure._id, buildLabel),
ItemType: treasure.ItemType,
Sockets: sockets
});
}
}
// Remove nemesis if it's using too new of a manifest to avoid script errors
if (
inventoryResponse.Nemesis &&
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
) {
inventoryResponse.Nemesis = undefined;
}
// U42 migrated ability paths so translate them back for older versions
if (version_compare(buildLabel, gameToBuildVersion["42.0.0"]) >= 0) {
return inventoryResponse;
}
for (const suit of inventoryResponse.Suits) {
for (const config of suit.Configs) {
if (config.AbilityOverride) {
config.AbilityOverride.Ability =
"/Lotus/Powersuits/PowersuitAbilities/" + config.AbilityOverride.Ability.split("/").pop();
}
}
}
// U40 migrated KubrowPetEggs to MiscItems so translate it back for older versions
if (version_compare(buildLabel, gameToBuildVersion["40.0.0"]) >= 0) {
return inventoryResponse;
}
inventoryResponse.KubrowPetEggs = [];
const index = inventoryResponse.MiscItems.findIndex(
x => x.ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg"
);
if (index != -1) {
const numKubrowEggs = Math.min(inventoryResponse.MiscItems[index].ItemCount, PRE_U40_MAX_KUBROW_EGGS);
inventoryResponse.MiscItems.splice(index, 1);
for (let i = 0; i != numKubrowEggs; ++i) {
inventoryResponse.KubrowPetEggs.push({
ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg",
ExpirationDate: toMongoDate2(2000000000000, buildLabel),
ItemId: toOid2(i.toString().padStart(24, "0"), buildLabel)
});
}
}
if (version_compare(buildLabel, gameToBuildVersion["38.5.0"]) >= 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
delete item.IsNew;
}
}
for (const item of inventoryResponse.WeaponSkins) {
delete item.IsNew;
}
if (version_compare(buildLabel, gameToBuildVersion["33.0.0"]) >= 0) {
return inventoryResponse;
}
// Old versions crash when faced with an unrecognised CollectibleType, so filter this array.
if (inventoryResponse.CollectibleSeries) {
inventoryResponse.CollectibleSeries = inventoryResponse.CollectibleSeries.filter(
x => x.CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne"
);
// U22.13.4 crashes if there's more than 64 characters + a '1' here.
for (const cs of inventoryResponse.CollectibleSeries) {
if (cs.Tracking.length > 64) {
cs.Tracking = cs.Tracking.substring(cs.Tracking.length - 64);
}
}
}
// U28.1 introduced Steel Path
if (version_compare(buildLabel, "2020.07.08.00.00") >= 0) {
return inventoryResponse;
}
for (const mission of inventoryResponse.Missions) {
delete mission.Tier;
}
if (version_compare(buildLabel, gameToBuildVersion["25.0.0"]) >= 0) {
return inventoryResponse;
}
for (const melee of inventoryResponse.Melee) {
if (melee.ItemType == "/Lotus/Weapons/Tenno/Melee/BrassKnuckles/BrassKnuckles") {
melee.ItemType = "/Lotus/Weapons/Tenno/Melee/Brass Knuckles/BrassKnuckles";
}
}
// Builds before U24.4.0 handle equipment features differently
if (version_compare(buildLabel, gameToBuildVersion["24.4.0"]) >= 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
if (item.Features && item.Features & eEquipmentFeatures.DOUBLE_CAPACITY) {
item.UnlockLevel = 1;
}
if (item.Features && item.Features & eEquipmentFeatures.UTILITY_SLOT) {
item.UtilityUnlocked = 1;
}
if (item.Features && item.Features & eEquipmentFeatures.GILDED) {
item.Gild = true;
}
}
}
// U15 ~ U18 are known to crash with unknown item types in XPInfo, so filter this array.
{
const prevLength = inventoryResponse.XPInfo.length;
inventoryResponse.XPInfo = inventoryResponse.XPInfo.filter(item => {
let introducedAt: number | undefined;
if (item.ItemType in ExportWarframes) {
introducedAt = ExportWarframes[item.ItemType].introducedAt;
} else if (item.ItemType in supplementalSuits) {
introducedAt = supplementalSuits[item.ItemType].introducedAt;
} else if (item.ItemType in ExportWeapons) {
introducedAt = ExportWeapons[item.ItemType].introducedAt;
}
if (!introducedAt) {
return false;
}
const introducedAtBuildVersion = wikiDateToBuildVersion(introducedAt);
return version_compare(buildLabel, introducedAtBuildVersion) >= 0;
});
if (prevLength != inventoryResponse.XPInfo.length) {
logger.debug(`omitting mastery info for ${prevLength - inventoryResponse.XPInfo.length} item(s)`);
}
}
// Before U22.14.0 Arcanes was installed directly at cosmetic items so client doesn't know about ranked up arcanes and UI displays them unproperly
if (version_compare(buildLabel, gameToBuildVersion["22.13.4"]) >= 0) {
return inventoryResponse;
}
inventoryResponse.Upgrades = inventoryResponse.Upgrades.filter(upgrade => !(upgrade.ItemType in ExportArcanes));
{
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
}
// U19.5 and below use $id instead of $oid as well as a different date format
if (version_compare(buildLabel, gameToBuildVersion["19.5.3"]) > 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
if (version_compare(buildLabel, gameToBuildVersion["16.5.5"]) < 0) {
// Appearance config format is different for versions before U16.5
for (const config of item.Configs) {
if (version_compare(buildLabel, gameToBuildVersion["16.0.2"]) < 0) {
config.Customization = {
Colors: convertIColorToLegacyColorsWithAtt(config.pricol, config.attcol),
Skins: config.Skins ?? []
};
} else if (version_compare(buildLabel, gameToBuildVersion["16.0.2"]) >= 0) {
config.Colors = convertIColorToLegacyColorsWithAtt(config.pricol, config.attcol);
}
}
}
}
}
// Deferring ids conversions for upgrades & skins for pre-U18.18 clients
if (version_compare(buildLabel, gameToBuildVersion["18.18.0"]) >= 0) {
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
for (const item of inventoryResponse.WeaponSkins) {
toLegacyOid(item.ItemId);
}
}
for (const ship of inventoryResponse.Ships) {
toLegacyOid(ship.ItemId);
}
for (const item of inventoryResponse.RawUpgrades) {
if (item.LastAdded) toLegacyOid(item.LastAdded);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
if (inventoryResponse.GuildId) {
toLegacyOid(inventoryResponse.GuildId);
}
for (const item of inventoryResponse.CurrentLoadOutIds) {
toLegacyOid(item);
}
if (version_compare(buildLabel, "2015.03.19.00.00") > 0) {
for (const category of loadoutKeysLegacy) {
if (category in inventoryResponse.LoadOutPresets!) {
for (const item of inventoryResponse.LoadOutPresets![category]) {
toLegacyOid(item.ItemId);
if (item.s?.ItemId) {
toLegacyOid(item.s.ItemId);
}
if (item.l?.ItemId) {
toLegacyOid(item.l.ItemId);
}
if (item.p?.ItemId) {
toLegacyOid(item.p.ItemId);
}
if (item.m?.ItemId) {
toLegacyOid(item.m.ItemId);
}
}
}
}
}
for (const item of inventoryResponse.Drones) {
toLegacyOid(item.ItemId);
}
for (const pr of inventoryResponse.PendingRecipes) {
toLegacyDate(pr.CompletionDate);
}
toLegacyDate(inventoryResponse.TrainingDate);
toLegacyDate(inventoryResponse.NextRefill!);
if (inventoryResponse.PersonalGoalProgress) {
for (const progress of inventoryResponse.PersonalGoalProgress) {
toLegacyOid(progress._id);
}
}
// Pre-U18.18 builds use a different UpgradeFingerprint format
if (version_compare(buildLabel, gameToBuildVersion["18.18.0"]) >= 0) {
return inventoryResponse;
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId); // deferred conversions of ids
const json = JSON.parse(upgrade.UpgradeFingerprint || '{"lvl":0}') as { lvl?: number };
const rank: number = json.lvl ?? 0;
upgrade.UpgradeFingerprint = convertToLegacyFingerprint(upgrade.UpgradeFingerprint || '{"lvl":0}');
if (
version_compare(buildLabel, gameToBuildVersion["7.3.0"]) >= 0 &&
version_compare(buildLabel, gameToBuildVersion["13.0.0"]) < 0
) {
// Pre-U10 builds
if (!upgrade.AmountRemaining || (upgrade.AmountRemaining && upgrade.AmountRemaining <= 0)) {
upgrade.AmountRemaining = 1;
}
upgrade.Rank = rank;
if (inventoryResponse.Cards) {
inventoryResponse.Cards.push(upgrade);
}
}
}
for (const skin of inventoryResponse.WeaponSkins) {
toLegacyOid(skin.ItemId); // deferred conversions of ids
if (skin.UpgradeType) {
skin.UpgradeFingerprint = convertToLegacyFingerprint(skin.UpgradeFingerprint || '{"lvl":0}');
}
}
// U18.16 merged left & right sentinel wings into the same skin item
if (version_compare(buildLabel, gameToBuildVersion["18.16.0"]) >= 0) {
return inventoryResponse;
}
const rightSkins = [];
for (const item of inventoryResponse.WeaponSkins) {
if (item.ItemType.startsWith("/Lotus/Upgrades/Skins/Sentinels/Wings/")) {
rightSkins.push(item.ItemType + "Right");
}
}
for (const itemType of rightSkins) {
inventoryResponse.WeaponSkins.push({
ItemType: itemType,
ItemId: { $id: "ca70ca70ca70ca70" + catBreadHash(itemType).toString(16).padStart(8, "0") }
});
}
if (version_compare(buildLabel, "2015.03.19.00.00") > 0) {
return inventoryResponse;
}
if (inventoryResponse.CurrentLoadOutIds.length > 0 && inventoryResponse.LoadOutPresets!.NORMAL.length > 0) {
if (version_compare(buildLabel, gameToBuildVersion["14.0.0"]) >= 0) {
// U14-U15
inventoryResponse.CurrentLoadout = {
$id: fromOid(inventoryResponse.CurrentLoadOutIds[0])
};
inventoryResponse.LoadoutPresets = [
mapLegacyLoadoutConfig(inventory, inventoryResponse.LoadOutPresets!, buildLabel)
] as unknown as FlattenMaps<ILoadoutConfigClientLegacy[]>;
} else {
// U13 and below
inventoryResponse.CurrentLoadout = mapLegacyLoadoutConfig(
inventory,
inventoryResponse.LoadOutPresets!,
buildLabel
);
}
}
delete inventoryResponse.LoadOutPresets;
// Pre-U12 builds store mods in an array called Cards, and have no concept of RawUpgrades
if (version_compare(buildLabel, "2014.02.05.00.00") >= 0) {
return inventoryResponse;
}
inventoryResponse.Cards = [];
if (version_compare(buildLabel, gameToBuildVersion["7.3.0"]) >= 0) {
for (const rawUpgrade of inventoryResponse.RawUpgrades) {
const id = inventory.RawUpgrades.find(x => x.ItemType == rawUpgrade.ItemType)?._id;
if (id) {
for (let i = 0; i < rawUpgrade.ItemCount; i++) {
const card = {
ItemType: rawUpgrade.ItemType,
ItemId: toOid2(id, buildLabel),
Rank: 0,
AmountRemaining: rawUpgrade.ItemCount
} as IUpgradeClient;
// Client doesn't see the mods unless they are in both Cards and Upgrades
inventoryResponse.Cards.push(card);
inventoryResponse.Upgrades.push(card);
}
}
}
}
inventoryResponse.RawUpgrades = [];
// Before U9, these aura mods were artifacts, ~U9.1.2 changed their paths
if (
version_compare(buildLabel, gameToBuildVersion["5.2.0"]) >= 0 &&
version_compare(buildLabel, gameToBuildVersion["9.1.2"]) < 0
) {
const supportedAuraMods = new Set([
"EnemyArmorReductionAuraMod",
"EnemyShieldReductionAuraMod",
"InfestationSpeedReductionAuraMod",
"PlayerElectricityImmunityAuraMod",
"PlayerFireImmunityAuraMod",
"PlayerFreezeImmunityAuraMod",
"PlayerLaserImmunityAuraMod",
"PlayerPoisonImmunityAuraMod",
"PlayerEnemyRadarAuraMod",
"PlayerLootRadarAuraMod",
"PlayerEnergyRegenAuraMod",
"PlayerHealthAuraMod",
"PlayerHealthRegenAuraMod",
"PlayerMeleeAuraMod",
"PlayerPistolAmmoAuraMod",
"PlayerPistolDamageAuraMod",
"PlayerRifleAmmoAuraMod",
"PlayerRifleDamageAuraMod",
"PlayerShellAmmoAuraMod",
"PlayerShellDamageAuraMod",
"PlayerSniperAmmoAuraMod",
"PlayerSniperDamageAuraMod",
"PlayerSprintAuraMod",
"PlayerXPAuraMod",
"RobotPoorAimAuraMod"
]);
for (const x of [...inventoryResponse.Cards, ...inventoryResponse.Upgrades]) {
const itemType = x.ItemType;
//console.log(itemType);
if (
!itemType.includes("/Lotus/Upgrades/Mods/Aura/") ||
!supportedAuraMods.has(itemType.slice(itemType.lastIndexOf("/") + 1))
)
continue;
x.ItemType = itemType
.replace("/Lotus/Upgrades/Mods/Aura/", "/Lotus/Types/Game/MissionBuffs/")
.replace("AuraMod", "Buff");
}
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
for (const config of item.Configs) {
if (config.Upgrades) {
// Convert installed upgrades for U10-U11
const convertedUpgrades: { $id: string }[] = [];
config.Upgrades.forEach(upgrade => {
const upgradeId = upgrade as string;
convertedUpgrades.push({ $id: upgradeId });
});
config.Upgrades = convertedUpgrades;
}
}
}
}
// Convert installed mods for pre-U12 builds
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
for (const config of item.Configs) {
if (config.Upgrades) {
for (let i = 0; i < config.Upgrades.length; i++) {
const id = config.Upgrades[i] as { $id: string | undefined };
const invUpgrade = inventoryResponse.Upgrades.find(x => x.ItemId.$id == id.$id);
if (invUpgrade) {
if (id.$id?.startsWith("/Lotus")) {
// Pre-U12 builds have no concept of RawUpgrades, have to convert the db entry to the closest id of an unranked copy
id.$id = inventoryResponse.Upgrades.find(x => x.ItemType == id.$id)?.ItemId.$id;
}
// Pre-U10
invUpgrade.ParentId = item.ItemId;
invUpgrade.Slot = i + 1;
}
}
}
}
}
}
// < U8 clients advertise gzip decompression support, but for inventory responses roughly >100k bytes, if we actually made use of that, it would buffer overrun.
if (version_compare(buildLabel, gameToBuildVersion["8.0.0"]) >= 0) {
return inventoryResponse;
}
request.headers["accept-encoding"] = "";
//(inventoryResponse as any).Deez = "a".repeat(100_000);
// U5 clients are very sensitive to unknown items
if (version_compare(buildLabel, gameToBuildVersion["7.3.0"]) >= 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
if (item.Features && item.Features & eEquipmentFeatures.DOUBLE_CAPACITY) {
item.UnlockLevel = 1;
}
}
}
const allowedMods = [
"/Lotus/Upgrades/Modules/GrineerMeleeModule",
"/Lotus/Upgrades/Modules/GrineerPistolModule",
"/Lotus/Upgrades/Modules/GrineerRifleModule",
"/Lotus/Upgrades/Modules/GrineerShotgunModule",
"/Lotus/Upgrades/Modules/OrokinWarframeModule",
"/Lotus/Upgrades/Modules/Crafted/IncendiaryRifleMod"
];
// for some reason they didn't shipped artifacts with U5.1
if (version_compare(buildLabel, gameToBuildVersion["5.2.0"]) >= 0) {
allowedMods.push("/Lotus/Upgrades/Modules/TennoSwordModule");
const oldMissionBuffs = [
"/Lotus/Types/Game/MissionBuffs/EnemyArmorReductionBuff",
"/Lotus/Types/Game/MissionBuffs/EnemyShieldReductionBuff",
"/Lotus/Types/Game/MissionBuffs/InfestationSpeedReductionBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerElectricityImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerFireImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerFreezeImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerLaserImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerPoisonImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerEnemyRadarBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerLootRadarBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerEnergyRegenBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerHealthBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerHealthRegenBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerMeleeBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerPistolAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerPistolDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerRifleAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerRifleDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerShellAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerShellDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerSniperAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerSniperDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerSprintBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerXPBuff",
"/Lotus/Types/Game/MissionBuffs/RobotPoorAimBuff"
];
inventoryResponse.Cards = inventoryResponse.Upgrades.filter(c => oldMissionBuffs.includes(c.ItemType));
}
inventoryResponse.Upgrades = inventoryResponse.Upgrades.filter(x => allowedMods.includes(x.ItemType));
inventoryResponse.Missions = inventoryResponse.Missions.filter(m => {
if (!m.Tag.startsWith("SolNode")) return false;
const n = Number.parseInt(m.Tag.slice(7), 10);
return n >= 1 && n <= 128;
});
inventoryResponse.Recipes = inventoryResponse.Recipes.map(recipe => {
const U5ItemType = modernToU5Recipes[recipe.ItemType];
return U5ItemType ? { ...recipe, ItemType: U5ItemType } : recipe;
}).filter(recipe => U5Recipes.includes(recipe.ItemType));
inventoryResponse.PendingRecipes = inventoryResponse.PendingRecipes.map(recipe => {
const U5ItemType = modernToU5Recipes[recipe.ItemType];
return U5ItemType ? { ...recipe, ItemType: U5ItemType } : recipe;
}).filter(recipe => U5Recipes.includes(recipe.ItemType));
const allowedMiscItems = [
"/Lotus/Types/Items/MiscItems/Actuator",
"/Lotus/Types/Items/MiscItems/AlloyPlate",
"/Lotus/Types/Items/MiscItems/Circuits",
"/Lotus/Types/Items/MiscItems/ControlModule",
"/Lotus/Types/Items/MiscItems/Ferrite",
"/Lotus/Types/Items/MiscItems/Gallium",
"/Lotus/Types/Items/MiscItems/Morphic",
"/Lotus/Types/Items/MiscItems/Nanospores",
"/Lotus/Types/Items/MiscItems/NeuralSensor",
"/Lotus/Types/Items/MiscItems/Neurode",
"/Lotus/Types/Items/MiscItems/OrokinCell",
"/Lotus/Types/Items/MiscItems/Plastids",
"/Lotus/Types/Items/MiscItems/PolymerBundle",
"/Lotus/Types/Items/MiscItems/Rubedo",
"/Lotus/Types/Items/MiscItems/Salvage"
];
inventoryResponse.MiscItems = inventoryResponse.MiscItems.filter(m => allowedMiscItems.includes(m.ItemType));
return inventoryResponse;
};
const mapLegacyLoadoutConfig = (
inventory: TInventoryDatabaseDocument,
loadoutPresets: ILoadOutPresets,
buildLabel: string
): ILoadoutConfigClientLegacy | undefined => {
// Loadout config mapping for U15 and below
const normPreset =
inventory.CurrentLoadOutIds[eLoadoutIndex.NORMAL] &&
loadoutPresets.NORMAL.find(x => inventory.CurrentLoadOutIds[eLoadoutIndex.NORMAL].equals(fromOid(x.ItemId)));
if (normPreset) {
const sId = normPreset.s?.ItemId && fromOid(normPreset.s.ItemId);
const pId = normPreset.p?.ItemId && fromOid(normPreset.p.ItemId);
const lId = normPreset.l?.ItemId && fromOid(normPreset.l.ItemId);
const mId = normPreset.m?.ItemId && fromOid(normPreset.m.ItemId);
const s = sId ? inventory.Suits.id(sId) : null;
const p = pId ? inventory.Pistols.id(pId) : null;
const l = lId ? inventory.LongGuns.id(lId) : null;
const m = mId ? inventory.Melee.id(mId) : null;
const loadoutConfig = {
ItemId: {
$id:
version_compare(buildLabel, gameToBuildVersion["14.0.0"]) < 0
? "Current"
: fromOid(normPreset.ItemId)
},
Name: normPreset.n ?? "Default Loadout",
Presets: [
{
ItemId: { $id: s?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.s?.mod ?? 0),
CustSlot: normPreset.s?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(s?.Configs[normPreset.s?.cus ?? 0]?.pricol),
Skins: s?.Configs[normPreset.s?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: p?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.p?.mod ?? 0),
CustSlot: normPreset.p?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(p?.Configs[normPreset.p?.cus ?? 0]?.pricol),
Skins: p?.Configs[normPreset.p?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: l?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.l?.mod ?? 0),
CustSlot: normPreset.l?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(l?.Configs[normPreset.l?.cus ?? 0]?.pricol),
Skins: l?.Configs[normPreset.l?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: m?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.m?.mod ?? 0),
CustSlot: normPreset.m?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(m?.Configs[normPreset.m?.cus ?? 0]?.pricol),
Skins: m?.Configs[normPreset.m?.cus ?? 0]?.Skins ?? []
}
}
]
};
if (version_compare(buildLabel, "2013.03.18.00.00") >= 0) {
const compPreset =
inventory.CurrentLoadOutIds[eLoadoutIndex.SENTINEL] &&
loadoutPresets.SENTINEL.find(x =>
inventory.CurrentLoadOutIds[eLoadoutIndex.SENTINEL].equals(fromOid(x.ItemId))
);
if (compPreset) {
const sId = compPreset.s?.ItemId && fromOid(compPreset.s.ItemId);
const lId = compPreset.l?.ItemId && fromOid(compPreset.l.ItemId);
const s = sId ? inventory.Sentinels.id(sId) : null;
const l = lId ? inventory.SentinelWeapons.id(lId) : null;
loadoutConfig.Presets.push(
{
ItemId: { $id: s?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (compPreset.s?.mod ?? 0),
CustSlot: compPreset.s?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(s?.Configs[compPreset.s?.cus ?? 0]?.pricol),
Skins: s?.Configs[compPreset.s?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: l?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (compPreset.l?.mod ?? 0),
CustSlot: compPreset.l?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(l?.Configs[compPreset.l?.cus ?? 0]?.pricol),
Skins: l?.Configs[compPreset.l?.cus ?? 0]?.Skins ?? []
}
}
);
} else {
logger.warn(
`Could not find SENTINEL loadout with id ${inventory.CurrentLoadOutIds[eLoadoutIndex.SENTINEL]?.toString()}, this part of the loadout will be empty`
);
loadoutConfig.Presets.push(
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
},
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
}
);
}
}
if (version_compare(buildLabel, gameToBuildVersion["15.0.0"]) >= 0) {
const archPreset =
inventory.CurrentLoadOutIds[eLoadoutIndex.ARCHWING] &&
loadoutPresets.ARCHWING.find(x =>
inventory.CurrentLoadOutIds[eLoadoutIndex.ARCHWING].equals(fromOid(x.ItemId))
);
if (archPreset) {
const sId = normPreset.s?.ItemId && fromOid(normPreset.s.ItemId);
const pId = normPreset.p?.ItemId && fromOid(normPreset.p.ItemId);
const mId = normPreset.m?.ItemId && fromOid(normPreset.m.ItemId);
const s = sId ? inventory.SpaceSuits.id(sId) : null;
const l = pId ? inventory.SpaceGuns.id(pId) : null;
const m = mId ? inventory.SpaceMelee.id(mId) : null;
loadoutConfig.Presets.push(
{
ItemId: { $id: s?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: archPreset.s?.mod ?? 0,
CustSlot: archPreset.s?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(s?.Configs[archPreset.s?.cus ?? 0]?.pricol),
Skins: s?.Configs[0]?.Skins ?? []
}
},
{
ItemId: { $id: l?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: archPreset.l?.mod ?? 0,
CustSlot: archPreset.l?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(l?.Configs[archPreset.l?.cus ?? 0]?.pricol),
Skins: l?.Configs[0]?.Skins ?? []
}
},
{
ItemId: { $id: m?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: archPreset.m?.mod ?? 0,
CustSlot: archPreset.m?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(m?.Configs[archPreset.m?.cus ?? 0]?.pricol),
Skins: m?.Configs[0]?.Skins ?? []
}
}
);
} else {
logger.warn(
`Could not find ARCHWING loadout with id ${inventory.CurrentLoadOutIds[eLoadoutIndex.ARCHWING]?.toString()}, this part of the loadout will be empty`
);
loadoutConfig.Presets.push(
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
},
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
},
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
}
);
}
}
return loadoutConfig;
}
logger.error(
`Could not find NORMAL loadout with id ${inventory.CurrentLoadOutIds[eLoadoutIndex.NORMAL]?.toString()}, entire loadout will be undefined`
);
return undefined;
};
const getExpRequiredForMr = (rank: number): number => {
if (rank <= 30) {
return 2500 * rank * rank;
}
return 2_250_000 + 147_500 * (rank - 30);
};
import type { Request, RequestHandler } from "express";
import { getAccountForRequest, getBuildLabel } from "../../services/loginService.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { config } from "../../services/configService.ts";
import allDialogue from "../../../static/fixed_responses/allDialogue.json" with { type: "json" };
import allPopups from "../../../static/fixed_responses/allPopups.json" with { type: "json" };
import type { ILoadoutConfigClientLegacy, ILoadoutDatabase, ILoadOutPresets } from "../../types/saveLoadoutTypes.ts";
import type {
IFusionTreasure,
IFusionTreasureClientLegacy,
IInventoryClient,
IShipInventory,
IUpgradeClient
} from "../../types/inventoryTypes/inventoryTypes.ts";
import {
accountCheatBooleans,
accountCheatNumbers,
eLoadoutIndex,
equipmentKeys,
loadoutKeysLegacy
} from "../../types/inventoryTypes/inventoryTypes.ts";
import type { IPolarity } from "../../types/inventoryTypes/commonInventoryTypes.ts";
import type { ICountedItem } from "warframe-public-export-plus";
import { ExportArcanes, ExportResources, ExportWarframes, ExportWeapons } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "../../services/infestedFoundryService.ts";
import {
addEmailItem,
addItem,
addMiscItems,
addMods,
allDailyAffiliationKeys,
checkCalendarAutoAdvance,
cleanupInventory,
createLibraryDailyTask,
dispatchPendingPremiumCredits,
ensureUserHasFounderHonoria,
ensureUserHasSteelPathRewards,
getCalendarProgress,
getInventory,
handleTauMemories,
PRE_U40_MAX_KUBROW_EGGS
} from "../../services/inventoryService.ts";
import { logger } from "../../utils/logger.ts";
import { addString, catBreadHash } from "../../helpers/stringHelpers.ts";
import { Types, type FlattenMaps } from "mongoose";
import { getNemesisManifest } from "../../helpers/nemesisHelpers.ts";
import { getPersonalRooms } from "../../services/personalRoomsService.ts";
import type { IPersonalRoomsClient } from "../../types/personalRoomsTypes.ts";
import { Ship } from "../../models/shipModel.ts";
import {
convertIColorToLegacyColors,
convertIColorToLegacyColorsWithAtt,
convertToLegacyFingerprint,
fromOid,
modernToU5Recipes,
toLegacyDate,
toLegacyOid,
toMongoDate2,
toOid,
toOid2,
U5Recipes,
version_compare
} from "../../helpers/inventoryHelpers.ts";
import { Inbox } from "../../models/inboxModel.ts";
import { unixTimesInMs } from "../../constants/timeConstants.ts";
import { DailyDeal } from "../../models/worldStateModel.ts";
import { eEquipmentFeatures } from "../../types/equipmentTypes.ts";
import { generateRewardSeed } from "../../services/rngService.ts";
import { getInvasionByOid, getWorldState } from "../../services/worldStateService.ts";
import { createMessage, getInboxFilter } from "../../services/inboxService.ts";
import gameToBuildVersion from "../../constants/gameToBuildVersion.ts";
import { PendingTrade } from "../../models/tradingModel.ts";
import { exportTrade } from "../../services/tradingService.ts";
import { supplementalSuits } from "../../services/itemDataService.ts";
import suitDefaultUpgrades from "../../constants/suitDefaultUpgrades.ts";
import type { ITypeCount } from "../../types/commonTypes.ts";
import { sendWsBroadcastToWebui } from "../../services/wsService.ts";
import { wikiDateToBuildVersion } from "../../helpers/versionHelper.ts";
export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request);
const buildLabel = getBuildLabel(request, account);
const inventory = await getInventory(account._id, undefined);
let sendUpdateForWebui = false;
// Handle daily reset
if (!inventory.NextRefill || Date.now() >= inventory.NextRefill.getTime()) {
const today = Math.trunc(Date.now() / 86400000);
for (const key of allDailyAffiliationKeys) {
inventory[key] = 16000 + inventory.PlayerLevel * 500;
}
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.TradesRemaining = inventory.PlayerLevel;
if (inventory.Founder) {
inventory.TradesRemaining += 2;
}
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(buildLabel);
if (inventory.NextRefill) {
const lastLoginDay = Math.trunc(inventory.NextRefill.getTime() / 86400000) - 1;
const daysPassed = today - lastLoginDay;
if (!inventory.noArgonCrystalDecay) {
let numArgonCrystals =
inventory.MiscItems.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0;
for (let i = 0; i != daysPassed; ++i) {
if (numArgonCrystals == 0) {
break;
}
const numStableArgonCrystals = Math.min(
numArgonCrystals,
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0
);
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
numArgonCrystals,
numStableArgonCrystals,
numDecayingArgonCrystals,
numDecayingArgonCrystalsToRemove
});
// Remove half of owned decaying argon crystals
addMiscItems(inventory, [
{
ItemType: "/Lotus/Types/Items/MiscItems/ArgonCrystal",
ItemCount: numDecayingArgonCrystalsToRemove * -1
}
]);
numArgonCrystals -= numDecayingArgonCrystalsToRemove;
// All stable argon crystals are now decaying
inventory.FoundToday = undefined;
}
}
if (inventory.UsedDailyDeals.length != 0) {
if (daysPassed == 1) {
const todayAt0Utc = today * 86400000;
const darvoIndex = Math.trunc((todayAt0Utc - 25200000) / (26 * unixTimesInMs.hour));
const darvoStart = darvoIndex * (26 * unixTimesInMs.hour) + 25200000;
const darvoOid =
((darvoStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "adc51a72f7324d95";
const deal = await DailyDeal.findById(darvoOid);
if (deal) {
inventory.UsedDailyDeals = inventory.UsedDailyDeals.filter(x => x == deal.StoreItem); // keep only the deal that came into this new day with us
} else {
inventory.UsedDailyDeals = [];
}
} else {
inventory.UsedDailyDeals = [];
}
}
for (const suit of inventory.Suits) {
if (suit.ExtraRemaining !== undefined) {
suit.ExtraRemaining = undefined;
}
}
}
// TODO: Setup CalendarProgress as part of 1999 mission completion?
const previousYearIteration = inventory.CalendarProgress?.Iteration;
// We need to do the following to ensure the in-game calendar does not break:
getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364)
checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424)
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress!.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues &&
!inventory.RetroDisableKissInboxMessage
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail, {}, buildLabel);
kalymos = false;
break;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem", {}, buildLabel);
}
}
if (inventory.Founder) {
await ensureUserHasFounderHonoria(inventory);
}
await ensureUserHasSteelPathRewards(inventory);
cleanupInventory(inventory);
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
//await inventory.save();
}
if (
inventory.InfestedFoundry &&
inventory.InfestedFoundry.AbilityOverrideUnlockCooldown &&
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) {
handleSubsumeCompletion(inventory);
//await inventory.save();
}
const forWebui = "wsid" in request.query;
if (inventory.pendingPremiumCredits && !forWebui) {
await dispatchPendingPremiumCredits(inventory);
//await inventory.save();
}
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: readonly ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
}
if (factionSidedWith && battlePay) {
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Language/Menu/GrineerInvasionLeader"
: "/Lotus/Language/Menu/CorpusInvasionLeader",
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
}
]);
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
{
const now = Date.now();
const lastSyncDuviriMood = Math.trunc((inventory.duviriSeedRefresh?.getTime() ?? 0) / 7200000);
const currentDuviriMood = Math.trunc(now / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
if (!inventory.DuviriInfo) {
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
inventory.duviriSeedRefresh = new Date(now);
} else {
inventory.DuviriInfo.Seed = generateRewardSeed();
inventory.duviriSeedRefresh = new Date(now);
}
}
}
if (inventory.QuestKeys.some(x => x.ItemType.endsWith("KubrowQuestKeyChain") && x.Completed)) {
for (const item of inventory.KubrowPets) {
item.Details.HasCollar = true;
}
}
await handleTauMemories(inventory);
if (
inventory.QuestKeys.some(
q => q.ItemType === "/Lotus/Types/Keys/JadeShadows/JadeShadowQuestKeyChain" && q.Completed == true
) &&
!inventory.NodeIntrosCompleted.some(i => i.startsWith("BabyName_"))
) {
inventory.NodeIntrosCompleted.push("BabyName_A");
logger.debug(`forcing baby name to Sirius`);
}
if (version_compare(buildLabel, gameToBuildVersion["15.0.0"]) < 0) {
const suitTypes = inventory.Suits.map(s => s.ItemType);
const rawUpgradeTypes = inventory.RawUpgrades.map(s => s.ItemType);
const upgradeTypes = inventory.Upgrades.map(s => s.ItemType);
const blacklist = new Set([...rawUpgradeTypes, ...upgradeTypes]);
const seen = new Set<string>();
const modsToAdd: ITypeCount[] = suitTypes.flatMap(suit =>
(suitDefaultUpgrades[suit] ?? []).flatMap(u => {
const itemType = u.ItemType;
if (blacklist.has(itemType) || seen.has(itemType)) return [];
seen.add(itemType);
return [
{
ItemType: itemType,
ItemCount: 1
}
];
})
);
if (modsToAdd.length > 0) {
logger.debug(`adding ${modsToAdd.length} missing legacy ability mods`);
addMods(inventory, modsToAdd);
sendUpdateForWebui = true;
}
}
if (inventory.isModified()) {
await inventory.save();
if (sendUpdateForWebui) sendWsBroadcastToWebui({ update_inventory: true }, account._id.toString());
}
response.json(
await getInventoryResponse(request, inventory, "xpBasedLevelCapDisabled" in request.query, buildLabel, forWebui)
);
};
export const getInventoryResponse = async (
request: Request,
inventory: TInventoryDatabaseDocument,
xpBasedLevelCapDisabled: boolean,
buildLabel: string,
forWebui: boolean = false
): Promise<IInventoryClient> => {
const [inventoryWithLoadOutPresets, ships, latestMessage, pendingTrades] = await Promise.all([
inventory.populate<{ LoadOutPresets: ILoadoutDatabase }>("LoadOutPresets"),
Ship.find({ ShipOwnerId: inventory.accountOwnerId }),
Inbox.findOne(getInboxFilter(inventory.accountOwnerId, buildLabel), "_id").sort({ date: -1 }),
PendingTrade.find({ $or: [{ a: inventory.accountOwnerId }, { b: inventory.accountOwnerId }] })
]);
const inventoryResponse = inventoryWithLoadOutPresets.toJSON<IInventoryClient>();
inventoryResponse.Ships = ships.map(x => x.toJSON<IShipInventory>());
inventoryResponse.PendingTrades = pendingTrades.map(trade => {
const us = trade.a.equals(inventory.accountOwnerId) ? "a" : "b";
return exportTrade(trade, us);
});
if (!forWebui) {
for (const key of accountCheatBooleans) {
delete inventoryResponse[key as keyof typeof inventoryResponse];
}
for (const key of accountCheatNumbers) {
delete inventoryResponse[key as keyof typeof inventoryResponse];
}
}
inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
// In case mission inventory update added an inbox message, we need to send the Mailbox part so the client knows to refresh it.
if (latestMessage) {
inventoryResponse.Mailbox = {
LastInboxId: toOid(latestMessage._id)
};
}
if (inventory.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999;
}
if (inventory.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 0;
inventoryResponse.PremiumCredits = 999999999;
}
if (inventory.infiniteEndo) {
inventoryResponse.FusionPoints = 999999999;
}
if (inventory.infiniteDirac) {
inventoryResponse.CrewShipFusionPoints = 999999999;
}
if (inventory.infiniteRegalAya) {
inventoryResponse.PrimeTokens = 999999999;
}
if (inventory.infiniteRevives) {
for (const suit of inventoryResponse.Suits) {
suit.ExtraRemaining = 999999999;
}
}
if (inventory.infiniteTrades) {
inventoryResponse.TradesRemaining = 9999;
}
if (inventory.infiniteGifts) {
inventoryResponse.GiftsRemaining = 9999;
}
if (inventory.skipAllDialogue) {
inventoryResponse.TauntHistory ??= [];
if (!inventoryResponse.TauntHistory.find(x => x.node == "TreasureTutorial")) {
inventoryResponse.TauntHistory.push({
node: "TreasureTutorial",
state: "TS_COMPLETED"
});
}
if (!inventoryResponse.TauntHistory.find(x => x.node == "StarchartTutorial")) {
inventoryResponse.TauntHistory.push({
node: "StarchartTutorial",
state: "TS_COMPLETED"
});
}
if (!inventoryResponse.TauntHistory.find(x => x.node == "CraftingTutorial")) {
inventoryResponse.TauntHistory.push({
node: "CraftingTutorial",
state: "TS_COMPLETED"
});
}
for (const str of allDialogue) {
addString(inventoryResponse.NodeIntrosCompleted, str);
}
}
if (inventory.skipAllPopups) {
for (const str of allPopups) {
addString(inventoryResponse.NodeIntrosCompleted, str);
}
inventoryResponse.Settings ??= {
FriendInvRestriction: "GIFT_MODE_ALL",
GiftMode: "GIFT_MODE_ALL",
GuildInvRestriction: "GIFT_MODE_ALL",
ShowFriendInvNotifications: true,
TradingRulesConfirmed: false
};
inventoryResponse.Settings.TradingRulesConfirmed = true;
inventoryResponse.PlayedParkourTutorial = true; // Skips PoE update popup in U22
inventoryResponse.MadeStoryModeDecision = true; // Skips tutorial popup in U15
inventoryResponse.HasResetAccount = true; // In ~U10, you are asked to confirm dojo contributions 3 times because "you will forfeit your one-time reset"
}
if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2026EarlyAccess"
].forEach(uniqueName => {
if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
});
}
if (inventory.spoofMasteryRank && inventory.spoofMasteryRank >= 0) {
inventoryResponse.PlayerLevel = inventory.spoofMasteryRank;
if (!xpBasedLevelCapDisabled) {
// This client has not been patched to accept any mastery rank, need to fake the XP.
// Limiting this to MR 100 to avoid breakage: https://onlyg.it/OpenWF/SpaceNinjaServer/issues/4244
inventoryResponse.XPInfo = [];
let numFrames = getExpRequiredForMr(Math.min(inventory.spoofMasteryRank, 100)) / (30 * 200);
while (numFrames-- > 0) {
inventoryResponse.XPInfo.push({
ItemType: "/Lotus/Powersuits/Mag/Mag",
XP: 900_000 // Enough for rank 30 as per https://wiki.warframe.com/w/Affinity
});
}
}
}
if (inventory.universalPolarityEverywhere) {
const Polarity: IPolarity[] = [];
// 12 is needed for necramechs. 15 is needed for plexus/crewshipharness.
for (let i = 0; i != 15; ++i) {
Polarity.push({
Slot: i,
Value: "AP_ANY"
});
}
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Polarity = Polarity;
}
}
}
}
if (inventory.unlockDoubleCapacityPotatoesEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= eEquipmentFeatures.DOUBLE_CAPACITY;
}
}
}
}
if (inventory.unlockExilusEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= eEquipmentFeatures.UTILITY_SLOT;
}
}
}
}
if (inventory.unlockArcanesEverywhere) {
for (const key of equipmentKeys) {
if (key in inventoryResponse) {
for (const equipment of inventoryResponse[key]) {
equipment.Features ??= 0;
equipment.Features |= eEquipmentFeatures.ARCANE_SLOT;
equipment.Features |= eEquipmentFeatures.SECOND_ARCANE_SLOT;
}
}
}
}
if (inventory.noDailyStandingLimits) {
const spoofedDailyAffiliation = Math.max(999_999, 16000 + inventoryResponse.PlayerLevel * 500);
for (const key of allDailyAffiliationKeys) {
inventoryResponse[key] = spoofedDailyAffiliation;
}
}
if (inventory.noDailyFocusLimit) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventory, inventoryResponse.InfestedFoundry);
}
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;
if (!forWebui) {
// Ensure numbers comfortably fit in a 32-bit integer so the client's in-memory value doesn't overflow or underflow.
for (const obj of inventory.RawUpgrades) {
if (obj.ItemCount > 999_999_999) {
obj.ItemCount = 999_999_999;
} else if (obj.ItemCount < -999_999_999) {
obj.ItemCount = -999_999_999;
}
}
}
// Convert fusion treasures to client format
inventoryResponse.FusionTreasures = [];
if (!buildLabel || version_compare(buildLabel, gameToBuildVersion["25.7.0"]) >= 0) {
for (const treasure of inventory.HybridFusionTreasures) {
const entry = (inventoryResponse.FusionTreasures as IFusionTreasure[]).find(
x => x.ItemType == treasure.ItemType && x.Sockets == treasure.Sockets
);
if (entry) {
++entry.ItemCount;
} else {
(inventoryResponse.FusionTreasures as IFusionTreasure[]).push({
ItemType: treasure.ItemType,
ItemCount: 1,
Sockets: treasure.Sockets
});
}
}
} else {
for (const treasure of inventory.HybridFusionTreasures) {
const meta = ExportResources[treasure.ItemType];
const sockets: Record<string, string> = {};
for (let i = 0; i != meta.sockets!.length; ++i) {
if ((treasure.Sockets >> i) & 1) {
sockets[String.fromCharCode(0x61 + i)] = meta.sockets![i];
}
}
(inventoryResponse.FusionTreasures as IFusionTreasureClientLegacy[]).push({
ItemId: toOid2(treasure._id, buildLabel),
ItemType: treasure.ItemType,
Sockets: sockets
});
}
}
// Remove nemesis if it's using too new of a manifest to avoid script errors
if (
inventoryResponse.Nemesis &&
version_compare(buildLabel, getNemesisManifest(inventoryResponse.Nemesis.manifest).minBuild) < 0
) {
inventoryResponse.Nemesis = undefined;
}
// U42 migrated ability paths so translate them back for older versions
if (version_compare(buildLabel, gameToBuildVersion["42.0.0"]) >= 0) {
return inventoryResponse;
}
for (const suit of inventoryResponse.Suits) {
for (const config of suit.Configs) {
if (config.AbilityOverride) {
config.AbilityOverride.Ability =
"/Lotus/Powersuits/PowersuitAbilities/" + config.AbilityOverride.Ability.split("/").pop();
}
}
}
// U40 migrated KubrowPetEggs to MiscItems so translate it back for older versions
if (version_compare(buildLabel, gameToBuildVersion["40.0.0"]) >= 0) {
return inventoryResponse;
}
inventoryResponse.KubrowPetEggs = [];
const index = inventoryResponse.MiscItems.findIndex(
x => x.ItemType == "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg"
);
if (index != -1) {
const numKubrowEggs = Math.min(inventoryResponse.MiscItems[index].ItemCount, PRE_U40_MAX_KUBROW_EGGS);
inventoryResponse.MiscItems.splice(index, 1);
for (let i = 0; i != numKubrowEggs; ++i) {
inventoryResponse.KubrowPetEggs.push({
ItemType: "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg",
ExpirationDate: toMongoDate2(2000000000000, buildLabel),
ItemId: toOid2(i.toString().padStart(24, "0"), buildLabel)
});
}
}
if (version_compare(buildLabel, gameToBuildVersion["38.5.0"]) >= 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
delete item.IsNew;
}
}
for (const item of inventoryResponse.WeaponSkins) {
delete item.IsNew;
}
if (version_compare(buildLabel, gameToBuildVersion["33.0.0"]) >= 0) {
return inventoryResponse;
}
// Old versions crash when faced with an unrecognised CollectibleType, so filter this array.
if (inventoryResponse.CollectibleSeries) {
inventoryResponse.CollectibleSeries = inventoryResponse.CollectibleSeries.filter(
x => x.CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne"
);
// U22.13.4 crashes if there's more than 64 characters + a '1' here.
for (const cs of inventoryResponse.CollectibleSeries) {
if (cs.Tracking.length > 64) {
cs.Tracking = cs.Tracking.substring(cs.Tracking.length - 64);
}
}
}
// U28.1 introduced Steel Path
if (version_compare(buildLabel, "2020.07.08.00.00") >= 0) {
return inventoryResponse;
}
for (const mission of inventoryResponse.Missions) {
delete mission.Tier;
}
if (version_compare(buildLabel, gameToBuildVersion["25.0.0"]) >= 0) {
return inventoryResponse;
}
for (const melee of inventoryResponse.Melee) {
if (melee.ItemType == "/Lotus/Weapons/Tenno/Melee/BrassKnuckles/BrassKnuckles") {
melee.ItemType = "/Lotus/Weapons/Tenno/Melee/Brass Knuckles/BrassKnuckles";
}
}
// Builds before U24.4.0 handle equipment features differently
if (version_compare(buildLabel, gameToBuildVersion["24.4.0"]) >= 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
if (item.Features && item.Features & eEquipmentFeatures.DOUBLE_CAPACITY) {
item.UnlockLevel = 1;
}
if (item.Features && item.Features & eEquipmentFeatures.UTILITY_SLOT) {
item.UtilityUnlocked = 1;
}
if (item.Features && item.Features & eEquipmentFeatures.GILDED) {
item.Gild = true;
}
}
}
// U15 ~ U18 are known to crash with unknown item types in XPInfo, so filter this array.
{
const prevLength = inventoryResponse.XPInfo.length;
inventoryResponse.XPInfo = inventoryResponse.XPInfo.filter(item => {
let introducedAt: number | undefined;
if (item.ItemType in ExportWarframes) {
introducedAt = ExportWarframes[item.ItemType].introducedAt;
} else if (item.ItemType in supplementalSuits) {
introducedAt = supplementalSuits[item.ItemType].introducedAt;
} else if (item.ItemType in ExportWeapons) {
introducedAt = ExportWeapons[item.ItemType].introducedAt;
}
if (!introducedAt) {
return false;
}
const introducedAtBuildVersion = wikiDateToBuildVersion(introducedAt);
return version_compare(buildLabel, introducedAtBuildVersion) >= 0;
});
if (prevLength != inventoryResponse.XPInfo.length) {
logger.debug(`omitting mastery info for ${prevLength - inventoryResponse.XPInfo.length} item(s)`);
}
}
// Before U22.14.0 Arcanes was installed directly at cosmetic items so client doesn't know about ranked up arcanes and UI displays them unproperly
if (version_compare(buildLabel, gameToBuildVersion["22.13.4"]) >= 0) {
return inventoryResponse;
}
inventoryResponse.Upgrades = inventoryResponse.Upgrades.filter(upgrade => !(upgrade.ItemType in ExportArcanes));
{
const personalRoomsDb = await getPersonalRooms(inventory.accountOwnerId.toString());
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
inventoryResponse.Ship = personalRooms.Ship;
}
// U19.5 and below use $id instead of $oid as well as a different date format
if (version_compare(buildLabel, gameToBuildVersion["19.5.3"]) > 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
toLegacyOid(item.ItemId);
if (version_compare(buildLabel, gameToBuildVersion["16.5.5"]) < 0) {
// Appearance config format is different for versions before U16.5
for (const config of item.Configs) {
if (version_compare(buildLabel, gameToBuildVersion["16.0.2"]) < 0) {
config.Customization = {
Colors: convertIColorToLegacyColorsWithAtt(config.pricol, config.attcol),
Skins: config.Skins ?? []
};
} else if (version_compare(buildLabel, gameToBuildVersion["16.0.2"]) >= 0) {
config.Colors = convertIColorToLegacyColorsWithAtt(config.pricol, config.attcol);
}
}
}
}
}
// Deferring ids conversions for upgrades & skins for pre-U18.18 clients
if (version_compare(buildLabel, gameToBuildVersion["18.18.0"]) >= 0) {
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId);
}
for (const item of inventoryResponse.WeaponSkins) {
toLegacyOid(item.ItemId);
}
}
for (const ship of inventoryResponse.Ships) {
toLegacyOid(ship.ItemId);
}
for (const item of inventoryResponse.RawUpgrades) {
if (item.LastAdded) toLegacyOid(item.LastAdded);
}
if (inventoryResponse.BrandedSuits) {
for (const id of inventoryResponse.BrandedSuits) {
toLegacyOid(id);
}
}
if (inventoryResponse.GuildId) {
toLegacyOid(inventoryResponse.GuildId);
}
for (const item of inventoryResponse.CurrentLoadOutIds) {
toLegacyOid(item);
}
if (version_compare(buildLabel, "2015.03.19.00.00") > 0) {
for (const category of loadoutKeysLegacy) {
if (category in inventoryResponse.LoadOutPresets!) {
for (const item of inventoryResponse.LoadOutPresets![category]) {
toLegacyOid(item.ItemId);
if (item.s?.ItemId) {
toLegacyOid(item.s.ItemId);
}
if (item.l?.ItemId) {
toLegacyOid(item.l.ItemId);
}
if (item.p?.ItemId) {
toLegacyOid(item.p.ItemId);
}
if (item.m?.ItemId) {
toLegacyOid(item.m.ItemId);
}
}
}
}
}
for (const item of inventoryResponse.Drones) {
toLegacyOid(item.ItemId);
}
for (const pr of inventoryResponse.PendingRecipes) {
toLegacyDate(pr.CompletionDate);
}
toLegacyDate(inventoryResponse.TrainingDate);
toLegacyDate(inventoryResponse.NextRefill!);
if (inventoryResponse.PersonalGoalProgress) {
for (const progress of inventoryResponse.PersonalGoalProgress) {
toLegacyOid(progress._id);
}
}
// Pre-U18.18 builds use a different UpgradeFingerprint format
if (version_compare(buildLabel, gameToBuildVersion["18.18.0"]) >= 0) {
return inventoryResponse;
}
for (const upgrade of inventoryResponse.Upgrades) {
toLegacyOid(upgrade.ItemId); // deferred conversions of ids
const json = JSON.parse(upgrade.UpgradeFingerprint || '{"lvl":0}') as { lvl?: number };
const rank: number = json.lvl ?? 0;
upgrade.UpgradeFingerprint = convertToLegacyFingerprint(upgrade.UpgradeFingerprint || '{"lvl":0}');
if (
version_compare(buildLabel, gameToBuildVersion["7.3.0"]) >= 0 &&
version_compare(buildLabel, gameToBuildVersion["13.0.0"]) < 0
) {
// Pre-U10 builds
if (!upgrade.AmountRemaining || (upgrade.AmountRemaining && upgrade.AmountRemaining <= 0)) {
upgrade.AmountRemaining = 1;
}
upgrade.Rank = rank;
if (inventoryResponse.Cards) {
inventoryResponse.Cards.push(upgrade);
}
}
}
for (const skin of inventoryResponse.WeaponSkins) {
toLegacyOid(skin.ItemId); // deferred conversions of ids
if (skin.UpgradeType) {
skin.UpgradeFingerprint = convertToLegacyFingerprint(skin.UpgradeFingerprint || '{"lvl":0}');
}
}
// U18.16 merged left & right sentinel wings into the same skin item
if (version_compare(buildLabel, gameToBuildVersion["18.16.0"]) >= 0) {
return inventoryResponse;
}
const rightSkins = [];
for (const item of inventoryResponse.WeaponSkins) {
if (item.ItemType.startsWith("/Lotus/Upgrades/Skins/Sentinels/Wings/")) {
rightSkins.push(item.ItemType + "Right");
}
}
for (const itemType of rightSkins) {
inventoryResponse.WeaponSkins.push({
ItemType: itemType,
ItemId: { $id: "ca70ca70ca70ca70" + catBreadHash(itemType).toString(16).padStart(8, "0") }
});
}
if (version_compare(buildLabel, "2015.03.19.00.00") > 0) {
return inventoryResponse;
}
if (inventoryResponse.CurrentLoadOutIds.length > 0 && inventoryResponse.LoadOutPresets!.NORMAL.length > 0) {
if (version_compare(buildLabel, gameToBuildVersion["14.0.0"]) >= 0) {
// U14-U15
inventoryResponse.CurrentLoadout = {
$id: fromOid(inventoryResponse.CurrentLoadOutIds[0])
};
inventoryResponse.LoadoutPresets = [
mapLegacyLoadoutConfig(inventory, inventoryResponse.LoadOutPresets!, buildLabel)
] as unknown as FlattenMaps<ILoadoutConfigClientLegacy[]>;
} else {
// U13 and below
inventoryResponse.CurrentLoadout = mapLegacyLoadoutConfig(
inventory,
inventoryResponse.LoadOutPresets!,
buildLabel
);
}
}
delete inventoryResponse.LoadOutPresets;
// Pre-U12 builds store mods in an array called Cards, and have no concept of RawUpgrades
if (version_compare(buildLabel, "2014.02.05.00.00") >= 0) {
return inventoryResponse;
}
inventoryResponse.Cards = [];
if (version_compare(buildLabel, gameToBuildVersion["7.3.0"]) >= 0) {
for (const rawUpgrade of inventoryResponse.RawUpgrades) {
const id = inventory.RawUpgrades.find(x => x.ItemType == rawUpgrade.ItemType)?._id;
if (id) {
for (let i = 0; i < rawUpgrade.ItemCount; i++) {
const card = {
ItemType: rawUpgrade.ItemType,
ItemId: toOid2(id, buildLabel),
Rank: 0,
AmountRemaining: rawUpgrade.ItemCount
} as IUpgradeClient;
// Client doesn't see the mods unless they are in both Cards and Upgrades
inventoryResponse.Cards.push(card);
inventoryResponse.Upgrades.push(card);
}
}
}
}
inventoryResponse.RawUpgrades = [];
// Before U9, these aura mods were artifacts, ~U9.1.2 changed their paths
if (
version_compare(buildLabel, gameToBuildVersion["5.2.0"]) >= 0 &&
version_compare(buildLabel, gameToBuildVersion["9.1.2"]) < 0
) {
const supportedAuraMods = new Set([
"EnemyArmorReductionAuraMod",
"EnemyShieldReductionAuraMod",
"InfestationSpeedReductionAuraMod",
"PlayerElectricityImmunityAuraMod",
"PlayerFireImmunityAuraMod",
"PlayerFreezeImmunityAuraMod",
"PlayerLaserImmunityAuraMod",
"PlayerPoisonImmunityAuraMod",
"PlayerEnemyRadarAuraMod",
"PlayerLootRadarAuraMod",
"PlayerEnergyRegenAuraMod",
"PlayerHealthAuraMod",
"PlayerHealthRegenAuraMod",
"PlayerMeleeAuraMod",
"PlayerPistolAmmoAuraMod",
"PlayerPistolDamageAuraMod",
"PlayerRifleAmmoAuraMod",
"PlayerRifleDamageAuraMod",
"PlayerShellAmmoAuraMod",
"PlayerShellDamageAuraMod",
"PlayerSniperAmmoAuraMod",
"PlayerSniperDamageAuraMod",
"PlayerSprintAuraMod",
"PlayerXPAuraMod",
"RobotPoorAimAuraMod"
]);
for (const x of [...inventoryResponse.Cards, ...inventoryResponse.Upgrades]) {
const itemType = x.ItemType;
//console.log(itemType);
if (
!itemType.includes("/Lotus/Upgrades/Mods/Aura/") ||
!supportedAuraMods.has(itemType.slice(itemType.lastIndexOf("/") + 1))
)
continue;
x.ItemType = itemType
.replace("/Lotus/Upgrades/Mods/Aura/", "/Lotus/Types/Game/MissionBuffs/")
.replace("AuraMod", "Buff");
}
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
for (const config of item.Configs) {
if (config.Upgrades) {
// Convert installed upgrades for U10-U11
const convertedUpgrades: { $id: string }[] = [];
config.Upgrades.forEach(upgrade => {
const upgradeId = upgrade as string;
convertedUpgrades.push({ $id: upgradeId });
});
config.Upgrades = convertedUpgrades;
}
}
}
}
// Convert installed mods for pre-U12 builds
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
for (const config of item.Configs) {
if (config.Upgrades) {
for (let i = 0; i < config.Upgrades.length; i++) {
const id = config.Upgrades[i] as { $id: string | undefined };
const invUpgrade = inventoryResponse.Upgrades.find(x => x.ItemId.$id == id.$id);
if (invUpgrade) {
if (id.$id?.startsWith("/Lotus")) {
// Pre-U12 builds have no concept of RawUpgrades, have to convert the db entry to the closest id of an unranked copy
id.$id = inventoryResponse.Upgrades.find(x => x.ItemType == id.$id)?.ItemId.$id;
}
// Pre-U10
invUpgrade.ParentId = item.ItemId;
invUpgrade.Slot = i + 1;
}
}
}
}
}
}
// < U8 clients advertise gzip decompression support, but for inventory responses roughly >100k bytes, if we actually made use of that, it would buffer overrun.
if (version_compare(buildLabel, gameToBuildVersion["8.0.0"]) >= 0) {
return inventoryResponse;
}
request.headers["accept-encoding"] = "";
//(inventoryResponse as any).Deez = "a".repeat(100_000);
// U5 clients are very sensitive to unknown items
if (version_compare(buildLabel, gameToBuildVersion["7.3.0"]) >= 0) {
return inventoryResponse;
}
for (const category of equipmentKeys) {
for (const item of inventoryResponse[category]) {
if (item.Features && item.Features & eEquipmentFeatures.DOUBLE_CAPACITY) {
item.UnlockLevel = 1;
}
}
}
const allowedMods = [
"/Lotus/Upgrades/Modules/GrineerMeleeModule",
"/Lotus/Upgrades/Modules/GrineerPistolModule",
"/Lotus/Upgrades/Modules/GrineerRifleModule",
"/Lotus/Upgrades/Modules/GrineerShotgunModule",
"/Lotus/Upgrades/Modules/OrokinWarframeModule",
"/Lotus/Upgrades/Modules/Crafted/IncendiaryRifleMod"
];
// for some reason they didn't shipped artifacts with U5.1
if (version_compare(buildLabel, gameToBuildVersion["5.2.0"]) >= 0) {
allowedMods.push("/Lotus/Upgrades/Modules/TennoSwordModule");
const oldMissionBuffs = [
"/Lotus/Types/Game/MissionBuffs/EnemyArmorReductionBuff",
"/Lotus/Types/Game/MissionBuffs/EnemyShieldReductionBuff",
"/Lotus/Types/Game/MissionBuffs/InfestationSpeedReductionBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerElectricityImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerFireImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerFreezeImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerLaserImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerPoisonImmunityBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerEnemyRadarBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerLootRadarBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerEnergyRegenBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerHealthBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerHealthRegenBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerMeleeBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerPistolAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerPistolDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerRifleAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerRifleDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerShellAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerShellDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerSniperAmmoBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerSniperDamageBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerSprintBuff",
"/Lotus/Types/Game/MissionBuffs/PlayerXPBuff",
"/Lotus/Types/Game/MissionBuffs/RobotPoorAimBuff"
];
inventoryResponse.Cards = inventoryResponse.Upgrades.filter(c => oldMissionBuffs.includes(c.ItemType));
}
inventoryResponse.Upgrades = inventoryResponse.Upgrades.filter(x => allowedMods.includes(x.ItemType));
inventoryResponse.Missions = inventoryResponse.Missions.filter(m => {
if (!m.Tag.startsWith("SolNode")) return false;
const n = Number.parseInt(m.Tag.slice(7), 10);
return n >= 1 && n <= 128;
});
inventoryResponse.Recipes = inventoryResponse.Recipes.map(recipe => {
const U5ItemType = modernToU5Recipes[recipe.ItemType];
return U5ItemType ? { ...recipe, ItemType: U5ItemType } : recipe;
}).filter(recipe => U5Recipes.includes(recipe.ItemType));
inventoryResponse.PendingRecipes = inventoryResponse.PendingRecipes.map(recipe => {
const U5ItemType = modernToU5Recipes[recipe.ItemType];
return U5ItemType ? { ...recipe, ItemType: U5ItemType } : recipe;
}).filter(recipe => U5Recipes.includes(recipe.ItemType));
const allowedMiscItems = [
"/Lotus/Types/Items/MiscItems/Actuator",
"/Lotus/Types/Items/MiscItems/AlloyPlate",
"/Lotus/Types/Items/MiscItems/Circuits",
"/Lotus/Types/Items/MiscItems/ControlModule",
"/Lotus/Types/Items/MiscItems/Ferrite",
"/Lotus/Types/Items/MiscItems/Gallium",
"/Lotus/Types/Items/MiscItems/Morphic",
"/Lotus/Types/Items/MiscItems/Nanospores",
"/Lotus/Types/Items/MiscItems/NeuralSensor",
"/Lotus/Types/Items/MiscItems/Neurode",
"/Lotus/Types/Items/MiscItems/OrokinCell",
"/Lotus/Types/Items/MiscItems/Plastids",
"/Lotus/Types/Items/MiscItems/PolymerBundle",
"/Lotus/Types/Items/MiscItems/Rubedo",
"/Lotus/Types/Items/MiscItems/Salvage"
];
inventoryResponse.MiscItems = inventoryResponse.MiscItems.filter(m => allowedMiscItems.includes(m.ItemType));
return inventoryResponse;
};
const mapLegacyLoadoutConfig = (
inventory: TInventoryDatabaseDocument,
loadoutPresets: ILoadOutPresets,
buildLabel: string
): ILoadoutConfigClientLegacy | undefined => {
// Loadout config mapping for U15 and below
const normPreset =
inventory.CurrentLoadOutIds[eLoadoutIndex.NORMAL] &&
loadoutPresets.NORMAL.find(x => inventory.CurrentLoadOutIds[eLoadoutIndex.NORMAL].equals(fromOid(x.ItemId)));
if (normPreset) {
const sId = normPreset.s?.ItemId && fromOid(normPreset.s.ItemId);
const pId = normPreset.p?.ItemId && fromOid(normPreset.p.ItemId);
const lId = normPreset.l?.ItemId && fromOid(normPreset.l.ItemId);
const mId = normPreset.m?.ItemId && fromOid(normPreset.m.ItemId);
const s = sId ? inventory.Suits.id(sId) : null;
const p = pId ? inventory.Pistols.id(pId) : null;
const l = lId ? inventory.LongGuns.id(lId) : null;
const m = mId ? inventory.Melee.id(mId) : null;
const loadoutConfig = {
ItemId: {
$id:
version_compare(buildLabel, gameToBuildVersion["14.0.0"]) < 0
? "Current"
: fromOid(normPreset.ItemId)
},
Name: normPreset.n ?? "Default Loadout",
Presets: [
{
ItemId: { $id: s?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.s?.mod ?? 0),
CustSlot: normPreset.s?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(s?.Configs[normPreset.s?.cus ?? 0]?.pricol),
Skins: s?.Configs[normPreset.s?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: p?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.p?.mod ?? 0),
CustSlot: normPreset.p?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(p?.Configs[normPreset.p?.cus ?? 0]?.pricol),
Skins: p?.Configs[normPreset.p?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: l?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.l?.mod ?? 0),
CustSlot: normPreset.l?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(l?.Configs[normPreset.l?.cus ?? 0]?.pricol),
Skins: l?.Configs[normPreset.l?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: m?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (normPreset.m?.mod ?? 0),
CustSlot: normPreset.m?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(m?.Configs[normPreset.m?.cus ?? 0]?.pricol),
Skins: m?.Configs[normPreset.m?.cus ?? 0]?.Skins ?? []
}
}
]
};
if (version_compare(buildLabel, "2013.03.18.00.00") >= 0) {
const compPreset =
inventory.CurrentLoadOutIds[eLoadoutIndex.SENTINEL] &&
loadoutPresets.SENTINEL.find(x =>
inventory.CurrentLoadOutIds[eLoadoutIndex.SENTINEL].equals(fromOid(x.ItemId))
);
if (compPreset) {
const sId = compPreset.s?.ItemId && fromOid(compPreset.s.ItemId);
const lId = compPreset.l?.ItemId && fromOid(compPreset.l.ItemId);
const s = sId ? inventory.Sentinels.id(sId) : null;
const l = lId ? inventory.SentinelWeapons.id(lId) : null;
loadoutConfig.Presets.push(
{
ItemId: { $id: s?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (compPreset.s?.mod ?? 0),
CustSlot: compPreset.s?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(s?.Configs[compPreset.s?.cus ?? 0]?.pricol),
Skins: s?.Configs[compPreset.s?.cus ?? 0]?.Skins ?? []
}
},
{
ItemId: { $id: l?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: version_compare(buildLabel, "2013.09.13.00.00") < 0 ? 0 : (compPreset.l?.mod ?? 0),
CustSlot: compPreset.l?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(l?.Configs[compPreset.l?.cus ?? 0]?.pricol),
Skins: l?.Configs[compPreset.l?.cus ?? 0]?.Skins ?? []
}
}
);
} else {
logger.warn(
`Could not find SENTINEL loadout with id ${inventory.CurrentLoadOutIds[eLoadoutIndex.SENTINEL]?.toString()}, this part of the loadout will be empty`
);
loadoutConfig.Presets.push(
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
},
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
}
);
}
}
if (version_compare(buildLabel, gameToBuildVersion["15.0.0"]) >= 0) {
const archPreset =
inventory.CurrentLoadOutIds[eLoadoutIndex.ARCHWING] &&
loadoutPresets.ARCHWING.find(x =>
inventory.CurrentLoadOutIds[eLoadoutIndex.ARCHWING].equals(fromOid(x.ItemId))
);
if (archPreset) {
const sId = normPreset.s?.ItemId && fromOid(normPreset.s.ItemId);
const pId = normPreset.p?.ItemId && fromOid(normPreset.p.ItemId);
const mId = normPreset.m?.ItemId && fromOid(normPreset.m.ItemId);
const s = sId ? inventory.SpaceSuits.id(sId) : null;
const l = pId ? inventory.SpaceGuns.id(pId) : null;
const m = mId ? inventory.SpaceMelee.id(mId) : null;
loadoutConfig.Presets.push(
{
ItemId: { $id: s?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: archPreset.s?.mod ?? 0,
CustSlot: archPreset.s?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(s?.Configs[archPreset.s?.cus ?? 0]?.pricol),
Skins: s?.Configs[0]?.Skins ?? []
}
},
{
ItemId: { $id: l?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: archPreset.l?.mod ?? 0,
CustSlot: archPreset.l?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(l?.Configs[archPreset.l?.cus ?? 0]?.pricol),
Skins: l?.Configs[0]?.Skins ?? []
}
},
{
ItemId: { $id: m?._id.toString() ?? "ffffffffffffffffffffffff" },
ModSlot: archPreset.m?.mod ?? 0,
CustSlot: archPreset.m?.cus ?? 0,
Customization: {
Emblem: "",
Colors: convertIColorToLegacyColors(m?.Configs[archPreset.m?.cus ?? 0]?.pricol),
Skins: m?.Configs[0]?.Skins ?? []
}
}
);
} else {
logger.warn(
`Could not find ARCHWING loadout with id ${inventory.CurrentLoadOutIds[eLoadoutIndex.ARCHWING]?.toString()}, this part of the loadout will be empty`
);
loadoutConfig.Presets.push(
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
},
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
},
{
ItemId: { $id: "ffffffffffffffffffffffff" },
ModSlot: 0,
CustSlot: 0,
Customization: {
Emblem: "",
Colors: [],
Skins: []
}
}
);
}
}
return loadoutConfig;
}
logger.error(
`Could not find NORMAL loadout with id ${inventory.CurrentLoadOutIds[eLoadoutIndex.NORMAL]?.toString()}, entire loadout will be undefined`
);
return undefined;
};
const getExpRequiredForMr = (rank: number): number => {
if (rank <= 30) {
return 2500 * rank * rank;
}
return 2_250_000 + 147_500 * (rank - 30);
};