From 7710e7c13fe3bf4a1c7e8d8bdbea7782c0b665ef Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:00:05 -0700 Subject: [PATCH 01/13] feat: inbox message for relics cracked during an unfinished mission (#2823) Closes #2821 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2823 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/loginController.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index beac741e..3a703b7b 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -9,6 +9,9 @@ import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../ import { logger } from "../../utils/logger.ts"; import { version_compare } from "../../helpers/inventoryHelpers.ts"; import { handleNonceInvalidation } from "../../services/wsService.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { createMessage } from "../../services/inboxService.ts"; +import { fromStoreItem } from "../../services/itemDataService.ts"; export const loginController: RequestHandler = async (request, response) => { const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object @@ -76,6 +79,24 @@ export const loginController: RequestHandler = async (request, response) => { handleNonceInvalidation(account._id.toString()); + // If the client crashed during an endless fissure mission, discharge rewards to an inbox message. (https://www.reddit.com/r/Warframe/comments/5uwwjm/til_if_you_crash_during_a_fissure_you_keep_any/) + const inventory = await getInventory(account._id.toString(), "MissionRelicRewards"); + if (inventory.MissionRelicRewards) { + await createMessage(account._id, [ + { + sndr: "/Lotus/Language/Bosses/Ordis", + msg: "/Lotus/Language/Menu/VoidProjectionItemsMessage", + sub: "/Lotus/Language/Menu/VoidProjectionItemsSubject", + icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", + countedAtt: inventory.MissionRelicRewards.map(x => ({ ...x, ItemType: fromStoreItem(x.ItemType) })), + attVisualOnly: true, + highPriority: true // TOVERIFY + } + ]); + inventory.MissionRelicRewards = undefined; + await inventory.save(); + } + response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); }; From 1c3f1e22765f5649092e0b90b2add0e62d665a41 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:00:17 -0700 Subject: [PATCH 02/13] feat: DeleteAllReadNonCin (#2824) Closes #2822 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2824 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/inboxController.ts | 9 +++++---- src/services/inboxService.ts | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 61911684..4f269794 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -4,6 +4,7 @@ import { createMessage, createNewEventMessages, deleteAllMessagesRead, + deleteAllMessagesReadNonCin, deleteMessageRead, getAllMessagesSorted, getMessage @@ -31,11 +32,11 @@ export const inboxController: RequestHandler = async (req, res) => { if (deleteId) { if (deleteId === "DeleteAllRead") { await deleteAllMessagesRead(accountId); - res.status(200).end(); - return; + } else if (deleteId === "DeleteAllReadNonCin") { + await deleteAllMessagesReadNonCin(accountId); + } else { + await deleteMessageRead(parseOid(deleteId as string)); } - - await deleteMessageRead(parseOid(deleteId as string)); res.status(200).end(); } else if (messageId) { const message = await getMessage(parseOid(messageId as string)); diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index 38053c0d..c5d80fba 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -29,6 +29,10 @@ export const deleteAllMessagesRead = async (accountId: string): Promise => await Inbox.deleteMany({ ownerId: accountId, r: true }); }; +export const deleteAllMessagesReadNonCin = async (accountId: string): Promise => { + await Inbox.deleteMany({ ownerId: accountId, r: true, cinematic: null }); +}; + export const createNewEventMessages = async (req: Request): Promise => { const account = await getAccountForRequest(req); const newEventMessages: IMessageCreationTemplate[] = []; From e5247700dfd8a6dce4f6936e3d4e80ef48a5331d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 29 Sep 2025 02:00:32 -0700 Subject: [PATCH 03/13] fix: use safe navigation to check for replay in giveKeyChainMessage (#2826) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2826 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/questService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/questService.ts b/src/services/questService.ts index a52f9c8d..02bf81b7 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -331,7 +331,7 @@ export const giveKeyChainMessage = async ( ): Promise => { const keyChainMessage = getKeyChainMessage(keyChainInfo); - if (questKey.Progress![0].c > 0) { + if ((questKey.Progress?.[0]?.c ?? 0) > 0) { keyChainMessage.att = []; keyChainMessage.countedAtt = []; } From 9426359370211325df777d780c6018d5b3de6d6a Mon Sep 17 00:00:00 2001 From: Gian Date: Mon, 29 Sep 2025 23:59:17 -0700 Subject: [PATCH 04/13] feat: Nights of Naberus (#2817) Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2817 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: Gian Co-committed-by: Gian --- config-vanilla.json | 1 + src/services/configService.ts | 1 + src/services/worldStateService.ts | 31 +++++++++++++++++++++++++++++++ static/webui/index.html | 8 ++++++++ static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/es.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/uk.js | 1 + static/webui/translations/zh.js | 1 + 11 files changed, 48 insertions(+) diff --git a/config-vanilla.json b/config-vanilla.json index 887a2141..b8810686 100644 --- a/config-vanilla.json +++ b/config-vanilla.json @@ -38,6 +38,7 @@ "anniversary": null, "hallowedNightmares": false, "hallowedNightmaresRewardsOverride": 0, + "naberusNightsOverride": null, "proxyRebellion": false, "proxyRebellionRewardsOverride": 0, "galleonOfGhouls": 0, diff --git a/src/services/configService.ts b/src/services/configService.ts index 238cb368..0c34e967 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -48,6 +48,7 @@ export interface IConfig { anniversary?: number; hallowedNightmares?: boolean; hallowedNightmaresRewardsOverride?: number; + naberusNightsOverride?: boolean; proxyRebellion?: boolean; proxyRebellionRewardsOverride?: number; galleonOfGhouls?: number; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index df35d988..716edee4 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -2504,6 +2504,37 @@ export const getWorldState = (buildLabel?: string): IWorldState => { BonusReward: { items: ["/Lotus/StoreItems/Upgrades/Skins/Clan/BountyHunterBadgeItem"] } }); } + + const isOctober = date.getUTCMonth() == 9; // October = month index 9 + if (config.worldState?.naberusNightsOverride ?? isOctober) { + worldState.Goals.push({ + _id: { $oid: "66fd602de1778d583419e8e7" }, + Activation: { + $date: { + $numberLong: config.worldState?.naberusNightsOverride + ? "1727881200000" + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1).toString() + } + }, + Expiry: { + $date: { + $numberLong: config.worldState?.naberusNightsOverride + ? "2000000000000" + : Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 1).toString() + } + }, + Count: 0, + Goal: 0, + Success: 0, + Personal: true, + Desc: "/Lotus/Language/Events/HalloweenNaberusName", + ToolTip: "/Lotus/Language/Events/HalloweenNaberusDesc", + Icon: "/Lotus/Interface/Icons/JackOLanternColour.png", + Tag: "DeimosHalloween", + Node: "DeimosHub" + }); + } + if (config.worldState?.bellyOfTheBeast) { worldState.Goals.push({ _id: { $oid: "67a5035c2a198564d62e165e" }, diff --git a/static/webui/index.html b/static/webui/index.html index 8c1d398d..ba8e4b78 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -1207,6 +1207,14 @@ +
+ + +
diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 6608ac2e..473294a0 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -284,6 +284,7 @@ dict = { worldState_hallowedFlame: `Geweihte Flamme`, worldState_hallowedNightmares: `Geweihte Albträume`, worldState_hallowedNightmaresRewards: `[UNTRANSLATED] Hallowed Nightmares Rewards`, + worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`, worldState_proxyRebellion: `Proxy-Rebellion`, worldState_proxyRebellionRewards: `[UNTRANSLATED] Proxy Rebellion Rewards`, worldState_bellyOfTheBeast: `Das Innere der Bestie`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 792ba3fe..cdb850fa 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -283,6 +283,7 @@ dict = { worldState_hallowedFlame: `Hallowed Flame`, worldState_hallowedNightmares: `Hallowed Nightmares`, worldState_hallowedNightmaresRewards: `Hallowed Nightmares Rewards`, + worldState_naberusNights: `Nights of Naberus`, worldState_proxyRebellion: `Proxy Rebellion`, worldState_proxyRebellionRewards: `Proxy Rebellion Rewards`, worldState_bellyOfTheBeast: `Belly of the Beast`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index cb7cb419..bee464df 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -284,6 +284,7 @@ dict = { worldState_hallowedFlame: `Llama Sagrada`, worldState_hallowedNightmares: `Pesadillas Sagradas`, worldState_hallowedNightmaresRewards: `Recompensas de Pesadillas Sagradas`, + worldState_naberusNights: `Noches de Naberus`, worldState_proxyRebellion: `Rebelión Proxy`, worldState_proxyRebellionRewards: `Recompensas de Rebelión Proxy`, worldState_bellyOfTheBeast: `Vientre de la Bestia`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index fbff6516..66fbad3c 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -284,6 +284,7 @@ dict = { worldState_hallowedFlame: `Flamme Hantée`, worldState_hallowedNightmares: `Cauchemars Hantés`, worldState_hallowedNightmaresRewards: `Récompenses Flamme Hantée Cauchemar`, + worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`, worldState_proxyRebellion: `Rébellion Proxy`, worldState_proxyRebellionRewards: `Récompenses Rébellion Proxy`, worldState_bellyOfTheBeast: `Ventre de la Bête`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 3174df2e..bdabbfb7 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -284,6 +284,7 @@ dict = { worldState_hallowedFlame: `Священное пламя`, worldState_hallowedNightmares: `Священные кошмары`, worldState_hallowedNightmaresRewards: `Награды Священных кошмаров`, + worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`, worldState_proxyRebellion: `Восстание роботов`, worldState_proxyRebellionRewards: `Награды Восстания роботов`, worldState_bellyOfTheBeast: `Чрево зверя`, diff --git a/static/webui/translations/uk.js b/static/webui/translations/uk.js index ff679a6a..7f5e38d9 100644 --- a/static/webui/translations/uk.js +++ b/static/webui/translations/uk.js @@ -284,6 +284,7 @@ dict = { worldState_hallowedFlame: `Священне полум'я`, worldState_hallowedNightmares: `Священні жахіття`, worldState_hallowedNightmaresRewards: `Нагороди Священних жахіть`, + worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`, worldState_proxyRebellion: `Повстання роботів`, worldState_proxyRebellionRewards: `Нагороди Повстання роботів`, worldState_bellyOfTheBeast: `У лігві звіра`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index e3d227ba..4d6495ec 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -284,6 +284,7 @@ dict = { worldState_hallowedFlame: `万圣之焰`, worldState_hallowedNightmares: `万圣噩梦`, worldState_hallowedNightmaresRewards: `万圣噩梦奖励设置`, + worldState_naberusNights: `[UNTRANSLATED] Nights of Naberus`, worldState_proxyRebellion: `机械叛乱`, worldState_proxyRebellionRewards: `机械叛乱奖励设置`, worldState_bellyOfTheBeast: `兽之腹`, From a8e41c95e73a9b3e26d7390561dbc9ea3ef51870 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:59:26 -0700 Subject: [PATCH 05/13] chore: move createNewEventMessages from inboxService to inboxController (#2828) This function wasn't used anywhere else and caused a recursive include in inboxService. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2828 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/inboxController.ts | 122 ++++++++++++++++++++++++- src/services/inboxService.ts | 120 +----------------------- 2 files changed, 120 insertions(+), 122 deletions(-) diff --git a/src/controllers/api/inboxController.ts b/src/controllers/api/inboxController.ts index 4f269794..d4308e65 100644 --- a/src/controllers/api/inboxController.ts +++ b/src/controllers/api/inboxController.ts @@ -1,13 +1,13 @@ -import type { RequestHandler } from "express"; +import type { Request, RequestHandler } from "express"; import { Inbox } from "../../models/inboxModel.ts"; import { createMessage, - createNewEventMessages, deleteAllMessagesRead, deleteAllMessagesReadNonCin, deleteMessageRead, getAllMessagesSorted, - getMessage + getMessage, + type IMessageCreationTemplate } from "../../services/inboxService.ts"; import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "../../services/loginService.ts"; import { @@ -22,6 +22,9 @@ import { ExportFlavour } from "warframe-public-export-plus"; import { handleStoreItemAcquisition } from "../../services/purchaseService.ts"; import { fromStoreItem, isStoreItem } from "../../services/itemDataService.ts"; import type { IOid } from "../../types/commonTypes.ts"; +import { unixTimesInMs } from "../../constants/timeConstants.ts"; +import { config } from "../../services/configService.ts"; +import { Types } from "mongoose"; export const inboxController: RequestHandler = async (req, res) => { const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query; @@ -135,6 +138,119 @@ export const inboxController: RequestHandler = async (req, res) => { } }; +const createNewEventMessages = async (req: Request): Promise => { + const account = await getAccountForRequest(req); + const newEventMessages: IMessageCreationTemplate[] = []; + + // Baro + const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); + const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000; + const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12); + if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) { + newEventMessages.push({ + sndr: "/Lotus/Language/G1Quests/VoidTraderName", + sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle", + msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage", + icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png", + startDate: new Date(baroActualStart), + endDate: new Date(baroStart + unixTimesInMs.day * 14), + CrossPlatform: true, + arg: [ + { + Key: "NODE_NAME", + Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4] + } + ], + date: new Date(baroActualStart) + }); + } + + // BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events. + const promises = []; + if (config.worldState?.creditBoost) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) { + newEventMessages.push({ + globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"), + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + sub: "/Lotus/Language/Items/EventDoubleCreditsName", + msg: "/Lotus/Language/Items/EventDoubleCreditsDesc", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + startDate: new Date(), + CrossPlatform: true + }); + } + })() + ); + } + if (config.worldState?.affinityBoost) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) { + newEventMessages.push({ + globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"), + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + sub: "/Lotus/Language/Items/EventDoubleAffinityName", + msg: "/Lotus/Language/Items/EventDoubleAffinityDesc", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + startDate: new Date(), + CrossPlatform: true + }); + } + })() + ); + } + if (config.worldState?.resourceBoost) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) { + newEventMessages.push({ + globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"), + sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", + sub: "/Lotus/Language/Items/EventDoubleResourceName", + msg: "/Lotus/Language/Items/EventDoubleResourceDesc", + icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", + startDate: new Date(), + CrossPlatform: true + }); + } + })() + ); + } + if (config.worldState?.galleonOfGhouls) { + promises.push( + (async (): Promise => { + if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) { + newEventMessages.push({ + sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek", + sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle", + msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc", + icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png", + transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek", + att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"], + startDate: new Date(), + goalTag: "GalleonRobbery" + }); + } + })() + ); + } + await Promise.all(promises); + + if (newEventMessages.length === 0) { + return; + } + + await createMessage(account._id, newEventMessages); + + const latestEventMessage = newEventMessages.reduce((prev, current) => + prev.startDate! > current.startDate! ? prev : current + ); + account.LatestEventMessageDate = new Date(latestEventMessage.startDate!); + await account.save(); +}; + // 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb const parseOid = (oid: string): string => { if (oid[0] == "{") { diff --git a/src/services/inboxService.ts b/src/services/inboxService.ts index c5d80fba..327afd9f 100644 --- a/src/services/inboxService.ts +++ b/src/services/inboxService.ts @@ -1,11 +1,6 @@ import type { IMessageDatabase } from "../models/inboxModel.ts"; import { Inbox } from "../models/inboxModel.ts"; -import { getAccountForRequest } from "./loginService.ts"; -import type { HydratedDocument } from "mongoose"; -import { Types } from "mongoose"; -import type { Request } from "express"; -import { unixTimesInMs } from "../constants/timeConstants.ts"; -import { config } from "./configService.ts"; +import type { HydratedDocument, Types } from "mongoose"; export const getAllMessagesSorted = async (accountId: string): Promise[]> => { const inbox = await Inbox.find({ ownerId: accountId }).sort({ date: -1 }); @@ -33,119 +28,6 @@ export const deleteAllMessagesReadNonCin = async (accountId: string): Promise => { - const account = await getAccountForRequest(req); - const newEventMessages: IMessageCreationTemplate[] = []; - - // Baro - const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); - const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000; - const baroActualStart = baroStart + unixTimesInMs.day * (config.worldState?.baroAlwaysAvailable ? 0 : 12); - if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) { - newEventMessages.push({ - sndr: "/Lotus/Language/G1Quests/VoidTraderName", - sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle", - msg: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceMessage", - icon: "/Lotus/Interface/Icons/Npcs/BaroKiTeerPortrait.png", - startDate: new Date(baroActualStart), - endDate: new Date(baroStart + unixTimesInMs.day * 14), - CrossPlatform: true, - arg: [ - { - Key: "NODE_NAME", - Tag: ["EarthHUB", "MercuryHUB", "SaturnHUB", "PlutoHUB"][baroIndex % 4] - } - ], - date: new Date(baroActualStart) - }); - } - - // BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events. - const promises = []; - if (config.worldState?.creditBoost) { - promises.push( - (async (): Promise => { - if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) { - newEventMessages.push({ - globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"), - sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", - sub: "/Lotus/Language/Items/EventDoubleCreditsName", - msg: "/Lotus/Language/Items/EventDoubleCreditsDesc", - icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", - startDate: new Date(), - CrossPlatform: true - }); - } - })() - ); - } - if (config.worldState?.affinityBoost) { - promises.push( - (async (): Promise => { - if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) { - newEventMessages.push({ - globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"), - sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", - sub: "/Lotus/Language/Items/EventDoubleAffinityName", - msg: "/Lotus/Language/Items/EventDoubleAffinityDesc", - icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", - startDate: new Date(), - CrossPlatform: true - }); - } - })() - ); - } - if (config.worldState?.resourceBoost) { - promises.push( - (async (): Promise => { - if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) { - newEventMessages.push({ - globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"), - sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender", - sub: "/Lotus/Language/Items/EventDoubleResourceName", - msg: "/Lotus/Language/Items/EventDoubleResourceDesc", - icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png", - startDate: new Date(), - CrossPlatform: true - }); - } - })() - ); - } - if (config.worldState?.galleonOfGhouls) { - promises.push( - (async (): Promise => { - if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) { - newEventMessages.push({ - sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek", - sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle", - msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc", - icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png", - transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek", - att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"], - startDate: new Date(), - goalTag: "GalleonRobbery" - }); - } - })() - ); - } - await Promise.all(promises); - - if (newEventMessages.length === 0) { - return; - } - - await createMessage(account._id, newEventMessages); - - const latestEventMessage = newEventMessages.reduce((prev, current) => - prev.startDate! > current.startDate! ? prev : current - ); - account.LatestEventMessageDate = new Date(latestEventMessage.startDate!); - await account.save(); -}; - export const createMessage = async ( accountId: string | Types.ObjectId, messages: IMessageCreationTemplate[] From c6a3e86d2b0cb9745c07e8955881e9c4037b0f7a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Mon, 29 Sep 2025 23:59:35 -0700 Subject: [PATCH 06/13] fix(webui): invoke giveKeyChainStageTriggered for new stage (#2830) Previously, this caused the old stage to just be reinitiated so we never went backwards. Closes #2829 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2830 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/manageQuestsController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index efd1ec63..25c80cd0 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -115,7 +115,7 @@ export const manageQuestsController: RequestHandler = async (req, res) => { if (stage > 0) { await giveKeyChainStageTriggered(inventory, { KeyChain: questKey.ItemType, - ChainStage: stage + ChainStage: stage - 1 }); } } From d65a667acddf3a598309bcd252c0707e89965ce3 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Tue, 30 Sep 2025 00:00:13 -0700 Subject: [PATCH 07/13] fix: ensure sorties show 'correct' image for corpus ice planet tileset (#2831) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2831 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 716edee4..57207050 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -361,7 +361,7 @@ export const getSortie = (day: number): ISortie => { Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", - Seed: seed, + Seed: 2081, // this seed produces 12 zeroes in a row if asked to pick (0, 1); this way the CorpusIcePlanetTileset image is always index 0, the 'correct' choice. Boss: boss, Variants: selectedNodes }; From 6e8800f048639d3159862e98df77946581ae4f86 Mon Sep 17 00:00:00 2001 From: Animan8000 Date: Wed, 1 Oct 2025 01:23:08 -0700 Subject: [PATCH 08/13] chore(webui): fix typos (#2832) also updated author credits Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2832 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: Animan8000 Co-committed-by: Animan8000 --- static/webui/translations/de.js | 4 ++-- static/webui/translations/en.js | 4 ++-- static/webui/translations/es.js | 2 +- static/webui/translations/zh.js | 6 +++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index 473294a0..d375c824 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -193,7 +193,7 @@ dict = { cheats_skipTutorial: `Tutorial überspringen`, cheats_skipAllDialogue: `Alle Dialoge überspringen`, cheats_unlockAllScans: `Alle Scans freischalten`, - cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please that you'll need to relog for the client to refresh this.`, + cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please note that you'll need to relog for the client to refresh this.`, cheats_unlockAllMissions: `Alle Missionen freischalten`, cheats_unlockAllMissions_ok: `Erfolgreich. Bitte beachte, dass du ein Dojo/Relais besuchen oder dich neu einloggen musst, damit die Sternenkarte aktualisiert wird.`, cheats_infiniteCredits: `Unendlich Credits`, @@ -227,7 +227,7 @@ dict = { cheats_baroFullyStocked: `Baro hat volles Inventar`, cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`, cheats_unlockAllProfitTakerStages: `Alle Profiteintreiber-Phasen freischalten`, - cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging..`, + cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index cdb850fa..fbf9dbee 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -192,7 +192,7 @@ dict = { cheats_skipTutorial: `Skip Tutorial`, cheats_skipAllDialogue: `Skip All Dialogue`, cheats_unlockAllScans: `Unlock All Scans`, - cheats_unlockSuccRelog: `Success. Please that you'll need to relog for the client to refresh this.`, + cheats_unlockSuccRelog: `Success. Please note that you'll need to relog for the client to refresh this.`, cheats_unlockAllMissions: `Unlock All Missions`, cheats_unlockAllMissions_ok: `Success. Please note that you'll need to enter a dojo/relay or relog for the client to refresh the star chart.`, cheats_infiniteCredits: `Infinite Credits`, @@ -226,7 +226,7 @@ dict = { cheats_baroFullyStocked: `Baro Fully Stocked`, cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`, cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`, - cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging..`, + cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index bee464df..888f16da 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -1,4 +1,4 @@ -// Spanish translation by hxedcl +// Spanish translation by hxedcl, Slayer55555 dict = { general_inventoryUpdateNote: `Para ver los cambios en el juego, necesitas volver a sincronizar tu inventario, por ejemplo, usando el comando /sync del bootstrapper, visitando un dojo o repetidor, o volviendo a iniciar sesión.`, general_inventoryUpdateNoteGameWs: `Nota: Puede que necesites reabrir cualquier menú en el que te encuentres para que los cambios se reflejen.`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 4d6495ec..c21843ae 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -1,4 +1,4 @@ -// Chinese translation by meb154, bishan178, nyaoouo, qianlishun, CrazyZhang, Corvus, & qingchun +// Chinese translation by meb154, bishan178, nyaoouo, qianlishun, CrazyZhang, Corvus, qingchun dict = { general_inventoryUpdateNote: `注意: 要在游戏中查看更改,您需要重新同步库存,例如使用客户端的 /sync 命令,访问道场/中继站或重新登录.`, general_inventoryUpdateNoteGameWs: `[UNTRANSLATED] Note: You may need to reopen any menu you are on for changes to be reflected.`, @@ -193,7 +193,7 @@ dict = { cheats_skipTutorial: `跳过教程`, cheats_skipAllDialogue: `跳过所有对话`, cheats_unlockAllScans: `解锁所有扫描`, - cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please that you'll need to relog for the client to refresh this.`, + cheats_unlockSuccRelog: `[UNTRANSLATED] Success. Please note that you'll need to relog for the client to refresh this.`, cheats_unlockAllMissions: `解锁所有星图`, cheats_unlockAllMissions_ok: `操作成功.请注意,您需要进入道场/中继站或重新登录以刷新星图数据.`, cheats_infiniteCredits: `无限现金`, @@ -227,7 +227,7 @@ dict = { cheats_baroFullyStocked: `虚空商人贩卖所有商品`, cheats_syndicateMissionsRepeatable: `集团任务可重复完成`, cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`, - cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging..`, + cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`, cheats_instantResourceExtractorDrones: `资源无人机即时完成`, cheats_noResourceExtractorDronesDamage: `资源无人机不会损毁`, From 8b3ee4b4f58be680126ceeaa7c9aad399dbdda82 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 2 Oct 2025 05:27:56 -0700 Subject: [PATCH 09/13] chore: allow sortie image randomisation for most tilesets (#2834) This should reduce the impact while we investigate #2833 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2834 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 57207050..4ee06990 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -361,7 +361,9 @@ export const getSortie = (day: number): ISortie => { Activation: { $date: { $numberLong: dayStart.toString() } }, Expiry: { $date: { $numberLong: dayEnd.toString() } }, Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards", - Seed: 2081, // this seed produces 12 zeroes in a row if asked to pick (0, 1); this way the CorpusIcePlanetTileset image is always index 0, the 'correct' choice. + Seed: selectedNodes.find(x => x.tileset == "CorpusIcePlanetTileset") + ? 2081 // this seed produces 12 zeroes in a row if asked to pick (0, 1); this way the CorpusIcePlanetTileset image is always index 0, the 'correct' choice. + : seed, Boss: boss, Variants: selectedNodes }; From 0136e4d152273abcfcaf1b8dd488cd4bfe6b0b59 Mon Sep 17 00:00:00 2001 From: LoseFace Date: Fri, 3 Oct 2025 06:45:57 -0700 Subject: [PATCH 10/13] chore(webui): clarify /sync command goes into chat (#2835) Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2835 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: LoseFace Co-committed-by: LoseFace --- static/webui/translations/de.js | 2 +- static/webui/translations/en.js | 4 ++-- static/webui/translations/zh.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index d375c824..b642b800 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -227,7 +227,7 @@ dict = { cheats_baroFullyStocked: `Baro hat volles Inventar`, cheats_syndicateMissionsRepeatable: `Syndikat-Missionen wiederholbar`, cheats_unlockAllProfitTakerStages: `Alle Profiteintreiber-Phasen freischalten`, - cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, + cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`, cheats_instantFinishRivenChallenge: `Riven-Mod Herausforderung sofort abschließen`, cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`, cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index fbf9dbee..57a32595 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -1,5 +1,5 @@ dict = { - general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, + general_inventoryUpdateNote: `Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`, general_inventoryUpdateNoteGameWs: `Note: You may need to reopen any menu you are on for changes to be reflected.`, general_addButton: `Add`, general_setButton: `Set`, @@ -226,7 +226,7 @@ dict = { cheats_baroFullyStocked: `Baro Fully Stocked`, cheats_syndicateMissionsRepeatable: `Syndicate Missions Repeatable`, cheats_unlockAllProfitTakerStages: `Unlock All Profit Taker Stages`, - cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, + cheats_unlockSuccInventory: `Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`, cheats_instantFinishRivenChallenge: `Instant Finish Riven Challenge`, cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`, cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index c21843ae..97a24244 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -227,7 +227,7 @@ dict = { cheats_baroFullyStocked: `虚空商人贩卖所有商品`, cheats_syndicateMissionsRepeatable: `集团任务可重复完成`, cheats_unlockAllProfitTakerStages: `解锁利润收割者圆蛛所有阶段`, - cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`, + cheats_unlockSuccInventory: `[UNTRANSLATED] Success. Please note that you'll need to resync your inventory, e.g. using the bootstrapper's /sync command in game chat, visiting a dojo/relay, or relogging.`, cheats_instantFinishRivenChallenge: `立即完成裂罅挑战`, cheats_instantResourceExtractorDrones: `资源无人机即时完成`, cheats_noResourceExtractorDronesDamage: `资源无人机不会损毁`, From 5772ebe74608a275b7074db244f47dd084d16d9d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 3 Oct 2025 06:46:07 -0700 Subject: [PATCH 11/13] feat(import): boosters (#2836) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2836 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/importService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/importService.ts b/src/services/importService.ts index 3783607f..e137396b 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -6,6 +6,7 @@ import type { } from "../types/inventoryTypes/commonInventoryTypes.ts"; import type { IMongoDate } from "../types/commonTypes.ts"; import type { + IBooster, IDialogueClient, IDialogueDatabase, IDialogueHistoryClient, @@ -463,6 +464,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< if (client.Accolades !== undefined) { db.Accolades = client.Accolades; } + if (client.Boosters !== undefined) { + replaceArray(db.Boosters, client.Boosters); + } }; export const importLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { From e67ef63b775c05ce66595b7e2b8a8580d2772016 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sat, 4 Oct 2025 04:18:21 -0700 Subject: [PATCH 12/13] fix: avoid using assassination node for an earlier sortie mission (#2838) Closes #2837 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2838 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 4ee06990..144698e9 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -280,6 +280,14 @@ export const getSortie = (day: number): ISortie => { } } + const willHaveAssassination = boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2; + if (willHaveAssassination) { + const index = nodes.indexOf(sortieBossNode[boss]); + if (index != -1) { + nodes.splice(index, 1); + } + } + const selectedNodes: ISortieMission[] = []; const missionTypes = new Set(); @@ -309,7 +317,7 @@ export const getSortie = (day: number): ISortie => { "SORTIE_MODIFIER_BOW_ONLY" ]; - if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) { + if (i == 2 && willHaveAssassination) { const tileset = sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets] as TSortieTileset; pushTilesetModifiers(modifiers, tileset); From 1ecf53c96b2f5e619961d1e64ff0903def9fb914 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 5 Oct 2025 05:56:58 -0700 Subject: [PATCH 13/13] chore: don't add 'alwaysAvailable' skins with unlockAllSkins (#2843) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2843 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/inventoryController.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index f4a9cfb1..76484e6f 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -335,15 +335,16 @@ export const getInventoryResponse = async ( } if (config.unlockAllSkins) { - const missingWeaponSkins = new Set(Object.keys(ExportCustoms)); - inventoryResponse.WeaponSkins.forEach(x => missingWeaponSkins.delete(x.ItemType)); - for (const uniqueName of missingWeaponSkins) { - inventoryResponse.WeaponSkins.push({ - ItemId: { - $oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0") - }, - ItemType: uniqueName - }); + const ownedWeaponSkins = new Set(inventoryResponse.WeaponSkins.map(x => x.ItemType)); + for (const [uniqueName, meta] of Object.entries(ExportCustoms)) { + if (!meta.alwaysAvailable && !ownedWeaponSkins.has(uniqueName)) { + inventoryResponse.WeaponSkins.push({ + ItemId: { + $oid: "ca70ca70ca70ca70" + catBreadHash(uniqueName).toString(16).padStart(8, "0") + }, + ItemType: uniqueName + }); + } } }